@hortonstudio/main 1.9.6 → 1.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoInit/accessibility/README.md +126 -0
- package/autoInit/accessibility/accessibility.js +56 -0
- package/autoInit/accessibility/functions/blog-remover/README.md +61 -0
- package/autoInit/accessibility/functions/blog-remover/blog-remover.js +31 -0
- package/autoInit/accessibility/functions/click-forwarding/README.md +60 -0
- package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +82 -0
- package/autoInit/accessibility/functions/convert-to-span/README.md +59 -0
- package/autoInit/accessibility/functions/convert-to-span/convert-to-span.js +70 -0
- package/autoInit/accessibility/functions/custom-values-replacement/README.md +71 -0
- package/autoInit/accessibility/functions/custom-values-replacement/custom-values-replacement.js +102 -0
- package/autoInit/accessibility/functions/dropdown/README.md +212 -0
- package/autoInit/accessibility/functions/dropdown/dropdown.js +167 -0
- package/autoInit/accessibility/functions/list-accessibility/README.md +56 -0
- package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +23 -0
- package/autoInit/accessibility/functions/prevent-default/README.md +58 -0
- package/autoInit/accessibility/functions/prevent-default/prevent-default.js +58 -0
- package/autoInit/accessibility/functions/remove-list-accessibility/README.md +57 -0
- package/autoInit/accessibility/functions/remove-list-accessibility/remove-list-accessibility.js +68 -0
- package/autoInit/accessibility/functions/text-synchronization/README.md +62 -0
- package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +101 -0
- package/autoInit/accessibility/functions/toc/README.md +79 -0
- package/autoInit/accessibility/functions/toc/toc.js +191 -0
- package/autoInit/accessibility/functions/year-replacement/README.md +54 -0
- package/autoInit/accessibility/functions/year-replacement/year-replacement.js +43 -0
- package/autoInit/button/README.md +122 -0
- package/autoInit/counter/README.md +274 -0
- package/autoInit/{counter.js → counter/counter.js} +20 -5
- package/autoInit/form/README.md +338 -0
- package/autoInit/{form.js → form/form.js} +44 -29
- package/autoInit/navbar/README.md +366 -0
- package/autoInit/site-settings/README.md +218 -0
- package/autoInit/smooth-scroll/README.md +386 -0
- package/autoInit/transition/README.md +301 -0
- package/autoInit/{transition.js → transition/transition.js} +13 -2
- package/index.js +7 -7
- package/package.json +1 -1
- package/autoInit/accessibility.js +0 -786
- /package/autoInit/{button.js → button/button.js} +0 -0
- /package/autoInit/{site-settings.js → site-settings/site-settings.js} +0 -0
- /package/autoInit/{smooth-scroll.js → smooth-scroll/smooth-scroll.js} +0 -0
package/autoInit/accessibility/functions/custom-values-replacement/custom-values-replacement.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export function init() {
|
|
2
|
+
function setupCustomValuesReplacement() {
|
|
3
|
+
const customValuesList = document.querySelector('[data-hs-a11y="custom-values-list"]');
|
|
4
|
+
|
|
5
|
+
if (!customValuesList) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Collect all custom values data
|
|
10
|
+
const customValues = {};
|
|
11
|
+
const descendants = customValuesList.getElementsByTagName('*');
|
|
12
|
+
|
|
13
|
+
Array.from(descendants).forEach(descendant => {
|
|
14
|
+
const nameElement = descendant.querySelector('[data-hs-a11y="custom-values-name"]');
|
|
15
|
+
const valueElement = descendant.querySelector('[data-hs-a11y="custom-values-value"]');
|
|
16
|
+
|
|
17
|
+
if (nameElement && valueElement) {
|
|
18
|
+
const name = nameElement.textContent.trim();
|
|
19
|
+
const value = valueElement.textContent.trim();
|
|
20
|
+
|
|
21
|
+
if (name && value) {
|
|
22
|
+
customValues[name] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// If no custom values found, exit early
|
|
28
|
+
if (Object.keys(customValues).length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Replace text content efficiently using TreeWalker
|
|
33
|
+
const walker = document.createTreeWalker(
|
|
34
|
+
document.body,
|
|
35
|
+
NodeFilter.SHOW_TEXT,
|
|
36
|
+
{
|
|
37
|
+
acceptNode: (node) => {
|
|
38
|
+
// Check if any custom value names exist in the text content
|
|
39
|
+
const text = node.textContent;
|
|
40
|
+
for (const name in customValues) {
|
|
41
|
+
if (text.includes(`{{${name}}}`)) {
|
|
42
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return NodeFilter.FILTER_SKIP;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const textNodes = [];
|
|
51
|
+
let node;
|
|
52
|
+
while (node = walker.nextNode()) {
|
|
53
|
+
textNodes.push(node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Replace text in collected nodes
|
|
57
|
+
textNodes.forEach(textNode => {
|
|
58
|
+
let newText = textNode.textContent;
|
|
59
|
+
let hasChanges = false;
|
|
60
|
+
|
|
61
|
+
for (const name in customValues) {
|
|
62
|
+
const placeholder = `{{${name}}}`;
|
|
63
|
+
if (newText.includes(placeholder)) {
|
|
64
|
+
newText = newText.replace(new RegExp(placeholder, 'g'), customValues[name]);
|
|
65
|
+
hasChanges = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (hasChanges) {
|
|
70
|
+
textNode.textContent = newText;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Replace link hrefs
|
|
75
|
+
const links = document.querySelectorAll('a[href]');
|
|
76
|
+
links.forEach(link => {
|
|
77
|
+
let href = link.getAttribute('href');
|
|
78
|
+
let hasChanges = false;
|
|
79
|
+
|
|
80
|
+
for (const name in customValues) {
|
|
81
|
+
const placeholder = `{{${name}}}`;
|
|
82
|
+
if (href.includes(placeholder)) {
|
|
83
|
+
href = href.replace(new RegExp(placeholder, 'g'), customValues[name]);
|
|
84
|
+
hasChanges = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (hasChanges) {
|
|
89
|
+
link.setAttribute('href', href);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setupCustomValuesReplacement();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
result: "custom-values-replacement initialized",
|
|
98
|
+
destroy: () => {
|
|
99
|
+
// No cleanup needed - this is a one-time text replacement
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# **Dropdown Accessibility**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
Universal dropdown system for FAQ, summary/read-more, and general toggle components. Automatically syncs ARIA attributes with Webflow interactions and optionally updates text content.
|
|
6
|
+
|
|
7
|
+
**This function handles all dropdown/accordion/toggle patterns with a single unified system.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## **Required Elements**
|
|
12
|
+
|
|
13
|
+
**Dropdown Wrapper**
|
|
14
|
+
* data-hs-dropdown="wrapper"
|
|
15
|
+
* data-hs-dropdown-text-swap="Close" (text for open state, defaults to "Close")
|
|
16
|
+
|
|
17
|
+
**Dropdown Toggle** *(gets is-active class from Webflow IX)*
|
|
18
|
+
* data-hs-dropdown="toggle"
|
|
19
|
+
* Contains clickable element
|
|
20
|
+
|
|
21
|
+
**Clickable Element**
|
|
22
|
+
* data-site-clickable="element"
|
|
23
|
+
* First child receives ARIA attributes
|
|
24
|
+
|
|
25
|
+
**Text Wrapper** *(optional - controls text update scope)*
|
|
26
|
+
* data-hs-dropdown="text"
|
|
27
|
+
* If present: all text inside swaps
|
|
28
|
+
* If absent: only aria-label updates (visual text stays static)
|
|
29
|
+
|
|
30
|
+
**Dropdown Content** *(expandable area)*
|
|
31
|
+
* data-hs-dropdown="content"
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## **What It Does**
|
|
36
|
+
|
|
37
|
+
1. **Monitors Toggle State**: MutationObserver watches for `is-active` class changes
|
|
38
|
+
2. **Updates ARIA**: Sets `aria-expanded`, `aria-controls`, `aria-hidden`, `role="region"`
|
|
39
|
+
3. **Text Updates**:
|
|
40
|
+
- **With text wrapper**: Swaps all text nodes + aria-label
|
|
41
|
+
- **Without text wrapper**: Only updates aria-label (visual stays static)
|
|
42
|
+
4. **Focus Management**: Returns focus to toggle button when closing if focus is inside content
|
|
43
|
+
5. **Supports Nesting**: Each dropdown manages its own state independently
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## **Usage Examples**
|
|
48
|
+
|
|
49
|
+
### **FAQ (No Visual Text Change)**
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close FAQ">
|
|
53
|
+
<div data-hs-dropdown="toggle">
|
|
54
|
+
<div data-site-clickable="element">
|
|
55
|
+
<button>
|
|
56
|
+
<span class="u-sr-only">Open FAQ</span>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<h3>What is this product?</h3>
|
|
60
|
+
</div>
|
|
61
|
+
<div data-hs-dropdown="content">
|
|
62
|
+
<p>This is a detailed answer...</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Result:**
|
|
68
|
+
- Question text stays visible ✅
|
|
69
|
+
- Only aria-label swaps: "Open FAQ" ↔ "Close FAQ"
|
|
70
|
+
- Perfect for FAQ lists
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### **Summary (Full Text Swap)**
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close">
|
|
78
|
+
<div data-hs-dropdown="toggle">
|
|
79
|
+
<p>View Quick Summary</p>
|
|
80
|
+
<div data-hs-dropdown="text">
|
|
81
|
+
<div data-site-clickable="element">
|
|
82
|
+
<button>View</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-hs-dropdown="content">
|
|
87
|
+
<p>Full summary content...</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Result:**
|
|
93
|
+
- All text in "text" wrapper swaps: "View" ↔ "Close"
|
|
94
|
+
- aria-label also swaps to match
|
|
95
|
+
- Perfect for read-more toggles
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### **Product Details (No Text Wrapper)**
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Hide Details">
|
|
103
|
+
<div data-hs-dropdown="toggle">
|
|
104
|
+
<div data-site-clickable="element">
|
|
105
|
+
<button>Show Details</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div data-hs-dropdown="content">
|
|
109
|
+
<p>Product specifications...</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Result:**
|
|
115
|
+
- Only aria-label swaps: "Show Details" ↔ "Hide Details"
|
|
116
|
+
- Visual button text stays static
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### **Nested Dropdowns**
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Hide">
|
|
124
|
+
<div data-hs-dropdown="toggle">
|
|
125
|
+
<button>Product Details</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div data-hs-dropdown="content">
|
|
128
|
+
<p>Product info...</p>
|
|
129
|
+
|
|
130
|
+
<!-- Nested FAQ -->
|
|
131
|
+
<div data-hs-dropdown="wrapper" data-hs-dropdown-text-swap="Close FAQ">
|
|
132
|
+
<div data-hs-dropdown="toggle">
|
|
133
|
+
<button>Shipping Questions</button>
|
|
134
|
+
</div>
|
|
135
|
+
<div data-hs-dropdown="content">
|
|
136
|
+
<p>Shipping details...</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Result:**
|
|
144
|
+
- Each dropdown manages its own state
|
|
145
|
+
- Focus returns to parent when parent closes
|
|
146
|
+
- Fully accessible keyboard navigation
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## **Key Attributes**
|
|
151
|
+
|
|
152
|
+
| Attribute | Purpose |
|
|
153
|
+
| ----- | ----- |
|
|
154
|
+
| `data-hs-dropdown="wrapper"` | Container for entire dropdown |
|
|
155
|
+
| `data-hs-dropdown-text-swap` | Text for open state (default: "Close") |
|
|
156
|
+
| `data-hs-dropdown="toggle"` | Gets is-active from Webflow IX |
|
|
157
|
+
| `data-hs-dropdown="text"` | Optional text update scope |
|
|
158
|
+
| `data-site-clickable="element"` | Contains actual button/link |
|
|
159
|
+
| `data-hs-dropdown="content"` | Expandable content area |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## **How It Works**
|
|
164
|
+
|
|
165
|
+
1. **Initial Setup**: Scans for all `[data-hs-dropdown="wrapper"]` elements
|
|
166
|
+
2. **ID Generation**: Creates unique IDs for ARIA relationships
|
|
167
|
+
3. **Text Capture**: Saves original text from text wrapper or clickable
|
|
168
|
+
4. **State Detection**: Checks for existing `is-active` class on toggle
|
|
169
|
+
5. **Observer Setup**: MutationObserver watches toggle for class changes
|
|
170
|
+
6. **State Sync**: When `is-active` changes, updates ARIA and text
|
|
171
|
+
7. **Focus Management**: Returns focus to button when closing if needed
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## **Focus Management**
|
|
176
|
+
|
|
177
|
+
When any dropdown closes:
|
|
178
|
+
- If focus is anywhere inside content area
|
|
179
|
+
- Focus automatically returns to toggle button
|
|
180
|
+
- Then ARIA updates apply
|
|
181
|
+
- Prevents keyboard users from losing focus
|
|
182
|
+
|
|
183
|
+
Works for:
|
|
184
|
+
- Buttons inside content ✅
|
|
185
|
+
- Nested dropdowns ✅
|
|
186
|
+
- Form fields inside content ✅
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## **Notes**
|
|
191
|
+
|
|
192
|
+
* Pattern matches navbar structure
|
|
193
|
+
* Works with Webflow IX for visual state (`is-active`)
|
|
194
|
+
* Text wrapper is optional - use for visual text changes
|
|
195
|
+
* Without text wrapper, only aria-label changes
|
|
196
|
+
* Supports infinite nesting depth
|
|
197
|
+
* Warns in console if multiple clickables found in toggle
|
|
198
|
+
* All observers cleaned up on destroy for Barba.js compatibility
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## **Common Patterns**
|
|
203
|
+
|
|
204
|
+
### **When to use text wrapper:**
|
|
205
|
+
- Summary/Read More toggles
|
|
206
|
+
- Buttons that should visually change
|
|
207
|
+
- Any toggle where text swap is visible
|
|
208
|
+
|
|
209
|
+
### **When NOT to use text wrapper:**
|
|
210
|
+
- FAQ items (question should stay visible)
|
|
211
|
+
- Toggles with static visible labels
|
|
212
|
+
- Dropdowns where only accessible text should change
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export function init() {
|
|
2
|
+
const cleanup = {
|
|
3
|
+
observers: []
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const addObserver = (observer) => cleanup.observers.push(observer);
|
|
7
|
+
|
|
8
|
+
function setupDropdownAccessibility(addObserver) {
|
|
9
|
+
const dropdownWrappers = document.querySelectorAll('[data-hs-dropdown="wrapper"]');
|
|
10
|
+
|
|
11
|
+
dropdownWrappers.forEach((wrapper, index) => {
|
|
12
|
+
const toggle = wrapper.querySelector('[data-hs-dropdown="toggle"]');
|
|
13
|
+
const content = wrapper.querySelector('[data-hs-dropdown="content"]');
|
|
14
|
+
const textSwapValue = wrapper.getAttribute('data-hs-dropdown-text-swap') || 'Close';
|
|
15
|
+
|
|
16
|
+
if (!toggle || !content) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check for multiple clickables and warn
|
|
21
|
+
const clickables = toggle.querySelectorAll('[data-site-clickable="element"]');
|
|
22
|
+
if (clickables.length > 1) {
|
|
23
|
+
console.warn('[dropdown-accessibility] Multiple clickables found in toggle. Using first one only.', toggle);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clickable = clickables[0];
|
|
27
|
+
if (!clickable) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const button = clickable.children[0];
|
|
32
|
+
if (!button) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate unique IDs
|
|
37
|
+
const buttonId = `hs-dropdown-btn-${index}`;
|
|
38
|
+
const contentId = `hs-dropdown-content-${index}`;
|
|
39
|
+
|
|
40
|
+
// Check if text wrapper exists
|
|
41
|
+
const textWrapper = toggle.querySelector('[data-hs-dropdown="text"]');
|
|
42
|
+
|
|
43
|
+
// Capture original text
|
|
44
|
+
let originalText = '';
|
|
45
|
+
if (textWrapper) {
|
|
46
|
+
// Get text from text wrapper
|
|
47
|
+
const walker = document.createTreeWalker(
|
|
48
|
+
textWrapper,
|
|
49
|
+
NodeFilter.SHOW_TEXT,
|
|
50
|
+
null,
|
|
51
|
+
false
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let firstTextNode = walker.nextNode();
|
|
55
|
+
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
56
|
+
firstTextNode = walker.nextNode();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
originalText = firstTextNode ? firstTextNode.textContent.trim() : textWrapper.textContent.trim();
|
|
60
|
+
} else {
|
|
61
|
+
// Get text from clickable for aria-label fallback
|
|
62
|
+
const walker = document.createTreeWalker(
|
|
63
|
+
clickable,
|
|
64
|
+
NodeFilter.SHOW_TEXT,
|
|
65
|
+
null,
|
|
66
|
+
false
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
let firstTextNode = walker.nextNode();
|
|
70
|
+
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
71
|
+
firstTextNode = walker.nextNode();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
originalText = firstTextNode ? firstTextNode.textContent.trim() : clickable.textContent.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Function to update text nodes
|
|
78
|
+
function updateText(newText) {
|
|
79
|
+
if (textWrapper) {
|
|
80
|
+
// Update all text nodes in text wrapper
|
|
81
|
+
const walker = document.createTreeWalker(
|
|
82
|
+
textWrapper,
|
|
83
|
+
NodeFilter.SHOW_TEXT,
|
|
84
|
+
null,
|
|
85
|
+
false
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const textNodes = [];
|
|
89
|
+
let node;
|
|
90
|
+
while (node = walker.nextNode()) {
|
|
91
|
+
if (node.textContent.trim()) {
|
|
92
|
+
textNodes.push(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
textNodes.forEach(textNode => {
|
|
97
|
+
textNode.textContent = newText;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Always update aria-label
|
|
102
|
+
button.setAttribute('aria-label', newText);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Set initial IDs and ARIA attributes
|
|
106
|
+
button.setAttribute('id', buttonId);
|
|
107
|
+
content.setAttribute('id', contentId);
|
|
108
|
+
content.setAttribute('role', 'region');
|
|
109
|
+
content.setAttribute('aria-labelledby', buttonId);
|
|
110
|
+
|
|
111
|
+
button.setAttribute('aria-controls', contentId);
|
|
112
|
+
|
|
113
|
+
// Function to check if dropdown is open
|
|
114
|
+
function isDropdownOpen() {
|
|
115
|
+
return toggle.classList.contains('is-active');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Update ARIA states based on current visual state
|
|
119
|
+
function updateARIAStates() {
|
|
120
|
+
const isOpen = isDropdownOpen();
|
|
121
|
+
const wasOpen = button.getAttribute("aria-expanded") === "true";
|
|
122
|
+
|
|
123
|
+
// If closing and focus is inside content, return focus first
|
|
124
|
+
if (wasOpen && !isOpen && content.contains(document.activeElement)) {
|
|
125
|
+
button.focus();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update ARIA attributes
|
|
129
|
+
button.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
130
|
+
content.setAttribute("aria-hidden", isOpen ? "false" : "true");
|
|
131
|
+
|
|
132
|
+
// Update text and aria-label
|
|
133
|
+
if (isOpen) {
|
|
134
|
+
updateText(textSwapValue);
|
|
135
|
+
} else {
|
|
136
|
+
updateText(originalText);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Set initial state based on existing is-active class
|
|
141
|
+
updateARIAStates();
|
|
142
|
+
|
|
143
|
+
// Monitor for class changes on toggle
|
|
144
|
+
const observer = new MutationObserver(() => {
|
|
145
|
+
updateARIAStates();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
observer.observe(toggle, {
|
|
149
|
+
attributes: true,
|
|
150
|
+
attributeFilter: ['class']
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
addObserver(observer);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setupDropdownAccessibility(addObserver);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
result: "dropdown initialized",
|
|
161
|
+
destroy: () => {
|
|
162
|
+
// Disconnect all observers
|
|
163
|
+
cleanup.observers.forEach(obs => obs.disconnect());
|
|
164
|
+
cleanup.observers.length = 0;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# **List Accessibility**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
Adds proper ARIA roles to custom-styled lists that aren't using semantic HTML list elements.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## **Required Elements**
|
|
10
|
+
|
|
11
|
+
**List Container**
|
|
12
|
+
* data-hs-a11y="list"
|
|
13
|
+
|
|
14
|
+
**List Items**
|
|
15
|
+
* data-hs-a11y="list-item"
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## **What It Does**
|
|
20
|
+
|
|
21
|
+
1. Finds all `[data-hs-a11y="list"]` elements
|
|
22
|
+
2. Adds `role="list"` to each
|
|
23
|
+
3. Finds all `[data-hs-a11y="list-item"]` elements
|
|
24
|
+
4. Adds `role="listitem"` to each
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## **Usage Example**
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<!-- Custom styled list -->
|
|
32
|
+
<div data-hs-a11y="list">
|
|
33
|
+
<div data-hs-a11y="list-item">Item 1</div>
|
|
34
|
+
<div data-hs-a11y="list-item">Item 2</div>
|
|
35
|
+
<div data-hs-a11y="list-item">Item 3</div>
|
|
36
|
+
</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Result:** Screen readers announce as a list with 3 items.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## **Key Attributes**
|
|
44
|
+
|
|
45
|
+
| Attribute | Purpose |
|
|
46
|
+
| ----- | ----- |
|
|
47
|
+
| `data-hs-a11y="list"` | List container |
|
|
48
|
+
| `data-hs-a11y="list-item"` | Individual list items |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## **Notes**
|
|
53
|
+
|
|
54
|
+
* Use when not using `<ul>/<ol>/<li>` semantic HTML
|
|
55
|
+
* ARIA roles remain after initialization
|
|
56
|
+
* No cleanup needed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function init() {
|
|
2
|
+
function setupListAccessibility() {
|
|
3
|
+
const listElements = document.querySelectorAll('[data-hs-a11y="list"]');
|
|
4
|
+
const listItemElements = document.querySelectorAll('[data-hs-a11y="list-item"]');
|
|
5
|
+
|
|
6
|
+
listElements.forEach(element => {
|
|
7
|
+
element.setAttribute('role', 'list');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
listItemElements.forEach(element => {
|
|
11
|
+
element.setAttribute('role', 'listitem');
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setupListAccessibility();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
result: "list-accessibility initialized",
|
|
19
|
+
destroy: () => {
|
|
20
|
+
// No cleanup needed - ARIA attributes remain on elements
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# **Prevent Default**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
Prevents default behavior on elements, including clicks and keyboard activation. Useful for decorative buttons or preventing anchor scrolling.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## **Required Elements**
|
|
10
|
+
|
|
11
|
+
**Element to Disable**
|
|
12
|
+
* data-hs-a11y="prevent-default"
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## **What It Does**
|
|
17
|
+
|
|
18
|
+
1. Finds all `[data-hs-a11y="prevent-default"]` elements
|
|
19
|
+
2. Prevents click events
|
|
20
|
+
3. Prevents Enter/Space keyboard activation
|
|
21
|
+
4. For anchor links with `#` hrefs:
|
|
22
|
+
- Removes href attribute
|
|
23
|
+
- Sets `role="button"`
|
|
24
|
+
- Sets `tabindex="0"`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## **Usage Example**
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<!-- Decorative button (no action) -->
|
|
32
|
+
<button data-hs-a11y="prevent-default">
|
|
33
|
+
Disabled Action
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<!-- Anchor link without scroll -->
|
|
37
|
+
<a href="#" data-hs-a11y="prevent-default">
|
|
38
|
+
Click me (won't scroll)
|
|
39
|
+
</a>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Result:** Elements receive focus but perform no action when clicked or activated.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## **Key Attributes**
|
|
47
|
+
|
|
48
|
+
| Attribute | Purpose |
|
|
49
|
+
| ----- | ----- |
|
|
50
|
+
| `data-hs-a11y="prevent-default"` | Element to disable |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## **Notes**
|
|
55
|
+
|
|
56
|
+
* Event listeners cleaned up on destroy
|
|
57
|
+
* Useful with Webflow IX for custom behaviors
|
|
58
|
+
* Anchor links converted to role="button"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function init() {
|
|
2
|
+
const cleanup = {
|
|
3
|
+
handlers: []
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const addHandler = (element, event, handler, options) => {
|
|
7
|
+
element.addEventListener(event, handler, options);
|
|
8
|
+
cleanup.handlers.push({ element, event, handler, options });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function setupPreventDefault(addHandler) {
|
|
12
|
+
const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
|
|
13
|
+
|
|
14
|
+
elements.forEach(element => {
|
|
15
|
+
// Prevent click
|
|
16
|
+
const clickHandler = (e) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
addHandler(element, 'click', clickHandler);
|
|
22
|
+
|
|
23
|
+
// Prevent keyboard activation
|
|
24
|
+
const keydownHandler = (e) => {
|
|
25
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
addHandler(element, 'keydown', keydownHandler);
|
|
32
|
+
|
|
33
|
+
// Additional prevention for anchor links
|
|
34
|
+
if (element.tagName.toLowerCase() === 'a') {
|
|
35
|
+
// Remove or modify href to prevent scroll
|
|
36
|
+
const originalHref = element.getAttribute('href');
|
|
37
|
+
if (originalHref && (originalHref === '#' || originalHref.startsWith('#'))) {
|
|
38
|
+
element.setAttribute('data-original-href', originalHref);
|
|
39
|
+
element.removeAttribute('href');
|
|
40
|
+
element.setAttribute('role', 'button');
|
|
41
|
+
element.setAttribute('tabindex', '0');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setupPreventDefault(addHandler);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
result: "prevent-default initialized",
|
|
51
|
+
destroy: () => {
|
|
52
|
+
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
53
|
+
element.removeEventListener(event, handler, options);
|
|
54
|
+
});
|
|
55
|
+
cleanup.handlers.length = 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|