@hortonstudio/main 1.9.6 → 1.9.8
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 +94 -0
- package/autoInit/accessibility/accessibility.js +52 -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/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/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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# **Accessibility System Documentation**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
The accessibility system provides 7 modular functions to enhance website accessibility and functionality. Each function operates independently and can be customized through data attributes.
|
|
6
|
+
|
|
7
|
+
**Note:** This module auto-initializes and loads all functions on page load.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## **Functions**
|
|
12
|
+
|
|
13
|
+
### **1. Blog Remover**
|
|
14
|
+
Automatically removes blog wrapper elements that have no blog list content.
|
|
15
|
+
|
|
16
|
+
**Use case:** Clean up empty blog sections when using Webflow CMS conditional visibility.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### **2. List Accessibility**
|
|
21
|
+
Adds proper ARIA `role="list"` and `role="listitem"` to custom-styled lists.
|
|
22
|
+
|
|
23
|
+
**Use case:** Making non-semantic list layouts accessible to screen readers.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### **3. Year Replacement**
|
|
28
|
+
Replaces `{{year}}` and `{{month}}` placeholders with current year and month.
|
|
29
|
+
|
|
30
|
+
**Use case:** Auto-updating copyright years and date-based content.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### **4. Click Forwarding**
|
|
35
|
+
Forwards clicks from decorative wrapper elements to actual interactive trigger elements.
|
|
36
|
+
|
|
37
|
+
**Use case:** Making entire card areas clickable while maintaining semantic structure.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### **5. Text Synchronization**
|
|
42
|
+
Synchronizes text content and aria-labels from original element to multiple match elements in real-time.
|
|
43
|
+
|
|
44
|
+
**Use case:** Keeping duplicate content in sync across multiple locations.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### **6. Table of Contents (TOC)**
|
|
49
|
+
Automatically generates a table of contents from H2 headings with smooth scrolling, focus management, and active state tracking.
|
|
50
|
+
|
|
51
|
+
**Use case:** Auto-generated TOC navigation for blog posts and documentation pages.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### **7. Dropdown**
|
|
56
|
+
Universal dropdown system for FAQ, summary/read-more, and general toggle components. Syncs ARIA with Webflow interactions and optionally updates text content.
|
|
57
|
+
|
|
58
|
+
**Use case:** All dropdown/accordion/toggle patterns with unified ARIA management and optional text swapping.
|
|
59
|
+
|
|
60
|
+
**Note:** This function replaces the legacy `faq-accessibility` and `summary` functions with a single unified system.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## **Documentation**
|
|
65
|
+
|
|
66
|
+
Each function has detailed documentation in its respective folder:
|
|
67
|
+
|
|
68
|
+
- `functions/blog-remover/README.md`
|
|
69
|
+
- `functions/list-accessibility/README.md`
|
|
70
|
+
- `functions/year-replacement/README.md`
|
|
71
|
+
- `functions/click-forwarding/README.md`
|
|
72
|
+
- `functions/text-synchronization/README.md`
|
|
73
|
+
- `functions/toc/README.md`
|
|
74
|
+
- `functions/dropdown/README.md`
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## **How It Works**
|
|
79
|
+
|
|
80
|
+
The accessibility system uses a dynamic loader pattern:
|
|
81
|
+
|
|
82
|
+
1. Main `accessibility.js` imports all function modules
|
|
83
|
+
2. Each function is loaded in parallel via `Promise.all()`
|
|
84
|
+
3. All destroy functions are collected for cleanup
|
|
85
|
+
4. On destroy, all functions are cleaned up properly
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## **Notes**
|
|
90
|
+
|
|
91
|
+
- All functions auto-initialize on page load
|
|
92
|
+
- Each function operates independently
|
|
93
|
+
- Barba.js compatible with proper cleanup
|
|
94
|
+
- No configuration required - works with data attributes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export async function init() {
|
|
2
|
+
// Centralized cleanup tracking
|
|
3
|
+
const cleanup = {
|
|
4
|
+
modules: {},
|
|
5
|
+
destroyFunctions: []
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const functionMap = {
|
|
9
|
+
"blog-remover": () => import("./functions/blog-remover/blog-remover.js"),
|
|
10
|
+
"list-accessibility": () => import("./functions/list-accessibility/list-accessibility.js"),
|
|
11
|
+
"year-replacement": () => import("./functions/year-replacement/year-replacement.js"),
|
|
12
|
+
"click-forwarding": () => import("./functions/click-forwarding/click-forwarding.js"),
|
|
13
|
+
"text-synchronization": () => import("./functions/text-synchronization/text-synchronization.js"),
|
|
14
|
+
"toc": () => import("./functions/toc/toc.js"),
|
|
15
|
+
"dropdown": () => import("./functions/dropdown/dropdown.js")
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const loadFunction = async (functionName) => {
|
|
19
|
+
try {
|
|
20
|
+
const { init } = await functionMap[functionName]();
|
|
21
|
+
const result = await init();
|
|
22
|
+
cleanup.modules[functionName] = result;
|
|
23
|
+
if (result && result.destroy) {
|
|
24
|
+
cleanup.destroyFunctions.push(result.destroy);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`Failed to load accessibility function: ${functionName}`, error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Load all functions
|
|
34
|
+
const functionPromises = Object.keys(functionMap).map(name => loadFunction(name));
|
|
35
|
+
await Promise.all(functionPromises);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
result: "accessibility initialized",
|
|
39
|
+
destroy: () => {
|
|
40
|
+
// Call all destroy functions
|
|
41
|
+
cleanup.destroyFunctions.forEach(destroyFn => {
|
|
42
|
+
try {
|
|
43
|
+
destroyFn();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error during accessibility cleanup:', error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
cleanup.destroyFunctions.length = 0;
|
|
49
|
+
cleanup.modules = {};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# **Blog Remover**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
Automatically removes blog wrapper elements that have no blog list content. Useful for cleaning up empty blog sections when using Webflow CMS conditional visibility.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## **Required Elements**
|
|
10
|
+
|
|
11
|
+
**Blog Wrapper**
|
|
12
|
+
* data-site-blog="wrapper"
|
|
13
|
+
* data-site-blog-config="delete-if-no-list" (triggers deletion check)
|
|
14
|
+
|
|
15
|
+
**Blog List** *(descendant of wrapper)*
|
|
16
|
+
* data-site-blog="list"
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## **What It Does**
|
|
21
|
+
|
|
22
|
+
1. Finds all `[data-site-blog="wrapper"]` elements
|
|
23
|
+
2. Checks if wrapper has `data-site-blog-config="delete-if-no-list"`
|
|
24
|
+
3. If yes, checks for descendant with `[data-site-blog="list"]`
|
|
25
|
+
4. If no list found, deletes the entire wrapper
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## **Usage Example**
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- Will be deleted if no blog list inside -->
|
|
33
|
+
<div data-site-blog="wrapper" data-site-blog-config="delete-if-no-list">
|
|
34
|
+
<h2>Recent Posts</h2>
|
|
35
|
+
<!-- If Webflow CMS hides the list, wrapper gets removed -->
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Will remain (no delete config) -->
|
|
39
|
+
<div data-site-blog="wrapper">
|
|
40
|
+
<h2>Recent Posts</h2>
|
|
41
|
+
<p>No posts yet.</p>
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## **Key Attributes**
|
|
48
|
+
|
|
49
|
+
| Attribute | Purpose |
|
|
50
|
+
| ----- | ----- |
|
|
51
|
+
| `data-site-blog="wrapper"` | Blog container |
|
|
52
|
+
| `data-site-blog-config="delete-if-no-list"` | Enable deletion if empty |
|
|
53
|
+
| `data-site-blog="list"` | Blog list (prevents deletion) |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## **Notes**
|
|
58
|
+
|
|
59
|
+
* One-time operation on page load
|
|
60
|
+
* No cleanup needed
|
|
61
|
+
* Barba.js compatible
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function init() {
|
|
2
|
+
function setupBlogListCleanup() {
|
|
3
|
+
const wrappers = document.querySelectorAll('[data-site-blog="wrapper"]');
|
|
4
|
+
|
|
5
|
+
wrappers.forEach(wrapper => {
|
|
6
|
+
// Check if wrapper has the delete-if-no-list config
|
|
7
|
+
const shouldDelete = wrapper.getAttribute('data-site-blog-config') === 'delete-if-no-list';
|
|
8
|
+
|
|
9
|
+
if (!shouldDelete) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check if there's a descendant with data-site-blog="list"
|
|
14
|
+
const hasList = wrapper.querySelector('[data-site-blog="list"]') !== null;
|
|
15
|
+
|
|
16
|
+
// Delete wrapper if it doesn't have a list
|
|
17
|
+
if (!hasList) {
|
|
18
|
+
wrapper.remove();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setupBlogListCleanup();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
result: "blog-remover initialized",
|
|
27
|
+
destroy: () => {
|
|
28
|
+
// No cleanup needed - this is a one-time DOM operation
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# **Click Forwarding**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
|
|
5
|
+
Forwards clicks from decorative/wrapper elements to actual interactive trigger elements. Useful for making entire card areas clickable while maintaining semantic button/link.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## **Required Elements**
|
|
10
|
+
|
|
11
|
+
**Clickable Element** *(wrapper users click)*
|
|
12
|
+
* data-hs-a11y="click-trigger-[identifier], clickable"
|
|
13
|
+
|
|
14
|
+
**Trigger Element** *(actual button/link)*
|
|
15
|
+
* data-hs-a11y="click-trigger-[identifier], trigger"
|
|
16
|
+
|
|
17
|
+
**Note:** `[identifier]` must match between clickable and trigger.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## **What It Does**
|
|
22
|
+
|
|
23
|
+
1. Finds all clickable elements
|
|
24
|
+
2. Matches them to trigger elements by identifier
|
|
25
|
+
3. Forwards click events from clickable → trigger
|
|
26
|
+
4. Adds keyboard support (Enter/Space)
|
|
27
|
+
5. Sets `tabindex="0"` and `role="button"` on clickable
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## **Usage Example**
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<div data-hs-a11y="click-trigger-card1, clickable">
|
|
35
|
+
<h3>Card Title</h3>
|
|
36
|
+
<p>Card description...</p>
|
|
37
|
+
<button data-hs-a11y="click-trigger-card1, trigger">
|
|
38
|
+
Learn More
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Result:** Clicking anywhere in the div triggers the button.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## **Key Attributes**
|
|
48
|
+
|
|
49
|
+
| Attribute | Purpose |
|
|
50
|
+
| ----- | ----- |
|
|
51
|
+
| `data-hs-a11y="click-trigger-[id], clickable"` | Wrapper to make clickable |
|
|
52
|
+
| `data-hs-a11y="click-trigger-[id], trigger"` | Actual button/link |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## **Notes**
|
|
57
|
+
|
|
58
|
+
* Identifier must match exactly
|
|
59
|
+
* Event listeners cleaned up on destroy
|
|
60
|
+
* Supports keyboard activation
|
|
@@ -0,0 +1,82 @@
|
|
|
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 setupClickForwarding(addHandler) {
|
|
12
|
+
// Find all clickable elements (custom styled elements users click)
|
|
13
|
+
const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
|
|
14
|
+
|
|
15
|
+
clickableElements.forEach(clickableElement => {
|
|
16
|
+
const attribute = clickableElement.getAttribute('data-hs-a11y');
|
|
17
|
+
|
|
18
|
+
// Parse the attribute: "click-trigger-[identifier], clickable"
|
|
19
|
+
const parts = attribute.split(',').map(part => part.trim());
|
|
20
|
+
|
|
21
|
+
// Find the part with click-trigger and the part with clickable
|
|
22
|
+
const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
|
|
23
|
+
const rolePart = parts.find(part => part === 'clickable');
|
|
24
|
+
|
|
25
|
+
if (!triggerPart || !rolePart) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Extract identifier from "click-trigger-[identifier]"
|
|
30
|
+
const identifier = triggerPart.replace('click-trigger-', '').trim();
|
|
31
|
+
|
|
32
|
+
// Find the corresponding trigger element
|
|
33
|
+
const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
|
|
34
|
+
const triggerElement = document.querySelector(triggerSelector);
|
|
35
|
+
|
|
36
|
+
if (!triggerElement) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add click event listener to forward clicks
|
|
41
|
+
const clickHandler = (event) => {
|
|
42
|
+
// Prevent default behavior on the clickable element
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
|
|
46
|
+
// Trigger click on the target element
|
|
47
|
+
triggerElement.click();
|
|
48
|
+
};
|
|
49
|
+
addHandler(clickableElement, 'click', clickHandler);
|
|
50
|
+
|
|
51
|
+
// Also handle keyboard events for accessibility
|
|
52
|
+
const keydownHandler = (event) => {
|
|
53
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
triggerElement.click();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
addHandler(clickableElement, 'keydown', keydownHandler);
|
|
60
|
+
|
|
61
|
+
// Ensure clickable element is keyboard accessible
|
|
62
|
+
if (!clickableElement.hasAttribute('tabindex')) {
|
|
63
|
+
clickableElement.setAttribute('tabindex', '0');
|
|
64
|
+
}
|
|
65
|
+
if (!clickableElement.hasAttribute('role')) {
|
|
66
|
+
clickableElement.setAttribute('role', 'button');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setupClickForwarding(addHandler);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
result: "click-forwarding initialized",
|
|
75
|
+
destroy: () => {
|
|
76
|
+
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
77
|
+
element.removeEventListener(event, handler, options);
|
|
78
|
+
});
|
|
79
|
+
cleanup.handlers.length = 0;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -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
|