@gnireeg/accordion 0.1.0
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/README.md +186 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @gnireeg/accordion
|
|
2
|
+
|
|
3
|
+
Accessible accordion web component with smooth animations, keyboard support, and nested accordion groups.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✨ Smooth expand/collapse animations
|
|
8
|
+
- ♿ Full accessibility (ARIA attributes, keyboard navigation)
|
|
9
|
+
- 🎨 Customizable animation timing and easing
|
|
10
|
+
- 📦 Zero dependencies
|
|
11
|
+
- 🎯 TypeScript support
|
|
12
|
+
- 🔄 Accordion groups with mutual exclusion
|
|
13
|
+
- 🪆 Support for nested accordions
|
|
14
|
+
- 🎪 Custom events for state changes
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @gnireeg/accordion
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Basic Accordion
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<script type="module">
|
|
28
|
+
import '@gnireeg/accordion';
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<accordion-item>
|
|
32
|
+
<button slot="trigger">Click to expand</button>
|
|
33
|
+
<div>Your content here</div>
|
|
34
|
+
</accordion-item>
|
|
35
|
+
|
|
36
|
+
<!-- Start expanded -->
|
|
37
|
+
<accordion-item open>
|
|
38
|
+
<button slot="trigger">Already open</button>
|
|
39
|
+
<div>This content is visible by default</div>
|
|
40
|
+
</accordion-item>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Accordion Group (Mutual Exclusion)
|
|
44
|
+
|
|
45
|
+
Wrap multiple accordion items in an `accordion-group` to ensure only one can be open at a time:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<accordion-group>
|
|
49
|
+
<accordion-item open>
|
|
50
|
+
<button slot="trigger">First Item</button>
|
|
51
|
+
<div>Only one item can be open at a time</div>
|
|
52
|
+
</accordion-item>
|
|
53
|
+
|
|
54
|
+
<accordion-item>
|
|
55
|
+
<button slot="trigger">Second Item</button>
|
|
56
|
+
<div>Opening this will close the first</div>
|
|
57
|
+
</accordion-item>
|
|
58
|
+
|
|
59
|
+
<accordion-item>
|
|
60
|
+
<button slot="trigger">Third Item</button>
|
|
61
|
+
<div>Same behavior here</div>
|
|
62
|
+
</accordion-item>
|
|
63
|
+
</accordion-group>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Custom Animation
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<accordion-item animation-time="500" animation-easing="ease-in-out">
|
|
70
|
+
<button slot="trigger">Slow animation</button>
|
|
71
|
+
<div>This opens and closes slower</div>
|
|
72
|
+
</accordion-item>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Programmatic Control
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const accordion = document.querySelector('accordion-item');
|
|
79
|
+
|
|
80
|
+
// Open the accordion
|
|
81
|
+
accordion.show();
|
|
82
|
+
|
|
83
|
+
// Close the accordion
|
|
84
|
+
accordion.close();
|
|
85
|
+
|
|
86
|
+
// Toggle open/closed state
|
|
87
|
+
accordion.toggle();
|
|
88
|
+
|
|
89
|
+
// Listen to events
|
|
90
|
+
accordion.addEventListener('accordion-opened', (e) => {
|
|
91
|
+
console.log('Opened!', e.detail);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
accordion.addEventListener('accordion-closed', (e) => {
|
|
95
|
+
console.log('Closed!', e.detail);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### `<accordion-item>`
|
|
102
|
+
|
|
103
|
+
#### Attributes
|
|
104
|
+
|
|
105
|
+
| Attribute | Type | Default | Description |
|
|
106
|
+
|-----------|------|---------|-------------|
|
|
107
|
+
| `open` | boolean | `false` | When present, the accordion starts in an expanded state |
|
|
108
|
+
| `animation-time` | string | `"300"` | Animation duration in milliseconds |
|
|
109
|
+
| `animation-easing` | string | `"ease"` | CSS easing function (e.g., `ease-in-out`, `cubic-bezier(...)`) |
|
|
110
|
+
|
|
111
|
+
#### Slots
|
|
112
|
+
|
|
113
|
+
| Slot | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `trigger` | The clickable element that toggles the accordion (typically a button) |
|
|
116
|
+
| (default) | The accordion content |
|
|
117
|
+
|
|
118
|
+
#### Methods
|
|
119
|
+
|
|
120
|
+
| Method | Description |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `show()` | Opens the accordion |
|
|
123
|
+
| `close()` | Closes the accordion |
|
|
124
|
+
| `toggle()` | Toggles between open and closed states |
|
|
125
|
+
|
|
126
|
+
#### Events
|
|
127
|
+
|
|
128
|
+
| Event | Detail | Description |
|
|
129
|
+
|-------|--------|-------------|
|
|
130
|
+
| `accordion-opened` | `{ open: true }` | Dispatched when the accordion opens |
|
|
131
|
+
| `accordion-closed` | `{ open: false }` | Dispatched when the accordion closes |
|
|
132
|
+
|
|
133
|
+
### `<accordion-group>`
|
|
134
|
+
|
|
135
|
+
#### Attributes
|
|
136
|
+
|
|
137
|
+
| Attribute | Type | Default | Description |
|
|
138
|
+
|-----------|------|---------|-------------|
|
|
139
|
+
| `allow-multiple-open` | boolean | `false` | When present, allows multiple accordions to be open simultaneously |
|
|
140
|
+
|
|
141
|
+
## Styling
|
|
142
|
+
|
|
143
|
+
### Styling the Open State
|
|
144
|
+
|
|
145
|
+
Use the `[open]` attribute selector to style accordion items when expanded:
|
|
146
|
+
|
|
147
|
+
```css
|
|
148
|
+
/* Rotate chevron icon when accordion is open */
|
|
149
|
+
accordion-item[open] [slot="trigger"] svg {
|
|
150
|
+
transform: rotate(180deg);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Change background color when open */
|
|
154
|
+
accordion-item[open] [slot="trigger"] {
|
|
155
|
+
background-color: #f1f5f9;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Add border accent when open */
|
|
159
|
+
accordion-item[open] {
|
|
160
|
+
border-left: 4px solid #3b82f6;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Accessibility
|
|
165
|
+
|
|
166
|
+
This component includes built-in accessibility features:
|
|
167
|
+
|
|
168
|
+
- Automatic `aria-expanded` attribute on trigger elements
|
|
169
|
+
- Keyboard support (Enter/Space) for non-button triggers
|
|
170
|
+
- Automatic `role="button"` for non-button triggers
|
|
171
|
+
- Screen reader friendly state announcements
|
|
172
|
+
|
|
173
|
+
## Browser Support
|
|
174
|
+
|
|
175
|
+
Works in all modern browsers that support:
|
|
176
|
+
- Custom Elements v1
|
|
177
|
+
- Shadow DOM v1
|
|
178
|
+
- ES Modules
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
183
|
+
|
|
184
|
+
## Author
|
|
185
|
+
|
|
186
|
+
Joel Geering
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccordionItem component - Expandable/collapsible content container with smooth animations
|
|
3
|
+
*
|
|
4
|
+
* @element accordion-item
|
|
5
|
+
*
|
|
6
|
+
* @attr {boolean} open - When present, the accordion starts in an expanded state
|
|
7
|
+
* @attr {string} animation-time - Animation duration in milliseconds (default: "300")
|
|
8
|
+
* @attr {string} animation-easing - CSS easing function (default: "ease")
|
|
9
|
+
*
|
|
10
|
+
* @slot trigger - The clickable element that toggles the accordion (typically a button)
|
|
11
|
+
* @slot - Default slot for the accordion content
|
|
12
|
+
*
|
|
13
|
+
* @fires accordion-opened - Dispatched when the accordion opens (detail: { open: true })
|
|
14
|
+
* @fires accordion-closed - Dispatched when the accordion closes (detail: { open: false })
|
|
15
|
+
*
|
|
16
|
+
* @cssprop [--animation-time] - Can be overridden via CSS custom properties
|
|
17
|
+
* @cssprop [--animation-easing] - Can be overridden via CSS custom properties
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <accordion-item>
|
|
22
|
+
* <button slot="trigger">Click to expand</button>
|
|
23
|
+
* <div>Your content here</div>
|
|
24
|
+
* </accordion-item>
|
|
25
|
+
*
|
|
26
|
+
* <!-- Start expanded with custom animation -->
|
|
27
|
+
* <accordion-item open animation-time="500" animation-easing="ease-in-out">
|
|
28
|
+
* <button slot="trigger">Already open</button>
|
|
29
|
+
* <div>This content is visible by default</div>
|
|
30
|
+
* </accordion-item>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```javascript
|
|
35
|
+
* // Programmatic control
|
|
36
|
+
* const accordion = document.querySelector('accordion-item');
|
|
37
|
+
* accordion.show(); // Open
|
|
38
|
+
* accordion.close(); // Close
|
|
39
|
+
* accordion.toggle(); // Toggle state
|
|
40
|
+
*
|
|
41
|
+
* // Listen to events
|
|
42
|
+
* accordion.addEventListener('accordion-opened', (e) => {
|
|
43
|
+
* console.log('Opened!', e.detail);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @note Automatically adds ARIA attributes (aria-expanded) for accessibility
|
|
48
|
+
* @note Supports keyboard navigation (Enter/Space) for non-button triggers
|
|
49
|
+
*/
|
|
50
|
+
export declare class AccordionItem extends HTMLElement {
|
|
51
|
+
private shadow;
|
|
52
|
+
private trigger;
|
|
53
|
+
private triggerElement;
|
|
54
|
+
open: boolean;
|
|
55
|
+
private animationTime;
|
|
56
|
+
private easing;
|
|
57
|
+
constructor();
|
|
58
|
+
static get observedAttributes(): string[];
|
|
59
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
60
|
+
connectedCallback(): void;
|
|
61
|
+
disconnectedCallback(): void;
|
|
62
|
+
private setupTriggerAccessibility;
|
|
63
|
+
private handleTriggerClick;
|
|
64
|
+
private handleKeydown;
|
|
65
|
+
/**
|
|
66
|
+
* Toggles the accordion between open and closed states
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
toggle: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Opens the accordion
|
|
72
|
+
* @public
|
|
73
|
+
* @fires accordion-opened
|
|
74
|
+
*/
|
|
75
|
+
show: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Closes the accordion
|
|
78
|
+
* @public
|
|
79
|
+
* @fires accordion-closed
|
|
80
|
+
*/
|
|
81
|
+
close: () => void;
|
|
82
|
+
private reflectState;
|
|
83
|
+
private dispatchStateEvent;
|
|
84
|
+
private updateTriggerAccessibility;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* AccordionGroup component - Container for managing multiple accordion items with mutual exclusion
|
|
88
|
+
*
|
|
89
|
+
* @element accordion-group
|
|
90
|
+
*
|
|
91
|
+
* @attr {boolean} allow-multiple-open - When present, allows multiple accordions to be open simultaneously. By default, opening one accordion closes all others in the group.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```html
|
|
95
|
+
* <!-- Only one accordion can be open at a time -->
|
|
96
|
+
* <accordion-group>
|
|
97
|
+
* <accordion-item open>
|
|
98
|
+
* <button slot="trigger">First Item</button>
|
|
99
|
+
* <div>Opening another will close this</div>
|
|
100
|
+
* </accordion-item>
|
|
101
|
+
*
|
|
102
|
+
* <accordion-item>
|
|
103
|
+
* <button slot="trigger">Second Item</button>
|
|
104
|
+
* <div>Only one can be open at a time</div>
|
|
105
|
+
* </accordion-item>
|
|
106
|
+
*
|
|
107
|
+
* <accordion-item>
|
|
108
|
+
* <button slot="trigger">Third Item</button>
|
|
109
|
+
* <div>Same behavior here</div>
|
|
110
|
+
* </accordion-item>
|
|
111
|
+
* </accordion-group>
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```html
|
|
116
|
+
* <!-- Allow multiple accordions to be open -->
|
|
117
|
+
* <accordion-group allow-multiple-open>
|
|
118
|
+
* <accordion-item>
|
|
119
|
+
* <button slot="trigger">First Item</button>
|
|
120
|
+
* <div>Can be open with others</div>
|
|
121
|
+
* </accordion-item>
|
|
122
|
+
*
|
|
123
|
+
* <accordion-item>
|
|
124
|
+
* <button slot="trigger">Second Item</button>
|
|
125
|
+
* <div>Multiple can be open</div>
|
|
126
|
+
* </accordion-item>
|
|
127
|
+
* </accordion-group>
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @note Each accordion-group works independently. Multiple groups on the same page don't affect each other.
|
|
131
|
+
* @note The group listens to 'accordion-opened' events from child accordion-item elements
|
|
132
|
+
*/
|
|
133
|
+
export declare class AccordionGroup extends HTMLElement {
|
|
134
|
+
private allowMultiple;
|
|
135
|
+
private static stylesApplied;
|
|
136
|
+
constructor();
|
|
137
|
+
static get observedAttributes(): string[];
|
|
138
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
139
|
+
connectedCallback(): void;
|
|
140
|
+
disconnectedCallback(): void;
|
|
141
|
+
private handleAccordionOpened;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAAa,aAAc,SAAQ,WAAW;IAE1C,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,cAAc,CAA4B;IAC3C,IAAI,UAAS;IACpB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAS;;IAwCvB,MAAM,KAAK,kBAAkB,aAE5B;IAED,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAiBvF,iBAAiB;IAajB,oBAAoB;IAWpB,OAAO,CAAC,yBAAyB,CAchC;IAED,OAAO,CAAC,kBAAkB,CAEzB;IAED,OAAO,CAAC,aAAa,CAKpB;IAED;;;OAGG;IACI,MAAM,aAMZ;IAED;;;;OAIG;IACI,IAAI,aAGV;IAED;;;;OAIG;IACI,KAAK,aAGX;IAED,OAAO,CAAC,YAAY,CAKnB;IAED,OAAO,CAAC,kBAAkB,CAOzB;IAED,OAAO,CAAC,0BAA0B,CAOjC;CACJ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,qBAAa,cAAe,SAAQ,WAAW;IAE3C,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,MAAM,CAAC,aAAa,CAAS;;IAarC,MAAM,KAAK,kBAAkB,aAE5B;IAED,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAevF,iBAAiB;IAGjB,oBAAoB;IAIpB,OAAO,CAAC,qBAAqB,CAS5B;CAEJ"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccordionItem component - Expandable/collapsible content container with smooth animations
|
|
3
|
+
*
|
|
4
|
+
* @element accordion-item
|
|
5
|
+
*
|
|
6
|
+
* @attr {boolean} open - When present, the accordion starts in an expanded state
|
|
7
|
+
* @attr {string} animation-time - Animation duration in milliseconds (default: "300")
|
|
8
|
+
* @attr {string} animation-easing - CSS easing function (default: "ease")
|
|
9
|
+
*
|
|
10
|
+
* @slot trigger - The clickable element that toggles the accordion (typically a button)
|
|
11
|
+
* @slot - Default slot for the accordion content
|
|
12
|
+
*
|
|
13
|
+
* @fires accordion-opened - Dispatched when the accordion opens (detail: { open: true })
|
|
14
|
+
* @fires accordion-closed - Dispatched when the accordion closes (detail: { open: false })
|
|
15
|
+
*
|
|
16
|
+
* @cssprop [--animation-time] - Can be overridden via CSS custom properties
|
|
17
|
+
* @cssprop [--animation-easing] - Can be overridden via CSS custom properties
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <accordion-item>
|
|
22
|
+
* <button slot="trigger">Click to expand</button>
|
|
23
|
+
* <div>Your content here</div>
|
|
24
|
+
* </accordion-item>
|
|
25
|
+
*
|
|
26
|
+
* <!-- Start expanded with custom animation -->
|
|
27
|
+
* <accordion-item open animation-time="500" animation-easing="ease-in-out">
|
|
28
|
+
* <button slot="trigger">Already open</button>
|
|
29
|
+
* <div>This content is visible by default</div>
|
|
30
|
+
* </accordion-item>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```javascript
|
|
35
|
+
* // Programmatic control
|
|
36
|
+
* const accordion = document.querySelector('accordion-item');
|
|
37
|
+
* accordion.show(); // Open
|
|
38
|
+
* accordion.close(); // Close
|
|
39
|
+
* accordion.toggle(); // Toggle state
|
|
40
|
+
*
|
|
41
|
+
* // Listen to events
|
|
42
|
+
* accordion.addEventListener('accordion-opened', (e) => {
|
|
43
|
+
* console.log('Opened!', e.detail);
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @note Automatically adds ARIA attributes (aria-expanded) for accessibility
|
|
48
|
+
* @note Supports keyboard navigation (Enter/Space) for non-button triggers
|
|
49
|
+
*/
|
|
50
|
+
export class AccordionItem extends HTMLElement {
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
this.trigger = null;
|
|
54
|
+
this.triggerElement = null;
|
|
55
|
+
this.open = false;
|
|
56
|
+
this.setupTriggerAccessibility = () => {
|
|
57
|
+
if (!this.trigger)
|
|
58
|
+
return;
|
|
59
|
+
this.triggerElement = this.trigger.assignedElements()[0];
|
|
60
|
+
if (!this.triggerElement)
|
|
61
|
+
return;
|
|
62
|
+
// If the slotted element is not a button, add keyboard support
|
|
63
|
+
if (this.triggerElement.tagName !== 'BUTTON' && !this.triggerElement.hasAttribute('role')) {
|
|
64
|
+
this.triggerElement.setAttribute('role', 'button');
|
|
65
|
+
this.triggerElement.setAttribute('tabindex', '0');
|
|
66
|
+
// Add keyboard listener
|
|
67
|
+
this.triggerElement.addEventListener('keydown', this.handleKeydown);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
this.handleTriggerClick = () => {
|
|
71
|
+
this.toggle();
|
|
72
|
+
};
|
|
73
|
+
this.handleKeydown = (e) => {
|
|
74
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
this.toggle();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Toggles the accordion between open and closed states
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
this.toggle = () => {
|
|
84
|
+
if (this.open) {
|
|
85
|
+
this.close();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.show();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Opens the accordion
|
|
93
|
+
* @public
|
|
94
|
+
* @fires accordion-opened
|
|
95
|
+
*/
|
|
96
|
+
this.show = () => {
|
|
97
|
+
this.open = true;
|
|
98
|
+
this.reflectState();
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Closes the accordion
|
|
102
|
+
* @public
|
|
103
|
+
* @fires accordion-closed
|
|
104
|
+
*/
|
|
105
|
+
this.close = () => {
|
|
106
|
+
this.open = false;
|
|
107
|
+
this.reflectState();
|
|
108
|
+
};
|
|
109
|
+
this.reflectState = () => {
|
|
110
|
+
// Update attribute to match property
|
|
111
|
+
this.open ? this.setAttribute('open', '') : this.removeAttribute('open');
|
|
112
|
+
this.updateTriggerAccessibility();
|
|
113
|
+
this.dispatchStateEvent();
|
|
114
|
+
};
|
|
115
|
+
this.dispatchStateEvent = () => {
|
|
116
|
+
// Dispatch custom events
|
|
117
|
+
this.dispatchEvent(new CustomEvent(this.open ? 'accordion-opened' : 'accordion-closed', {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
composed: true,
|
|
120
|
+
detail: { open: this.open }
|
|
121
|
+
}));
|
|
122
|
+
};
|
|
123
|
+
this.updateTriggerAccessibility = () => {
|
|
124
|
+
if (!this.trigger)
|
|
125
|
+
return;
|
|
126
|
+
const triggerElement = this.trigger.assignedElements()[0];
|
|
127
|
+
if (triggerElement) {
|
|
128
|
+
triggerElement.setAttribute('aria-expanded', String(this.open));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// Initialize state from attribute
|
|
132
|
+
this.open = this.hasAttribute('open');
|
|
133
|
+
this.animationTime = this.getAttribute('animation-time') || '300';
|
|
134
|
+
this.easing = this.getAttribute('animation-easing') || 'ease';
|
|
135
|
+
this.shadow = this.attachShadow({ mode: 'open' });
|
|
136
|
+
this.shadow.innerHTML = /*HTML*/ `
|
|
137
|
+
<style>
|
|
138
|
+
:host{
|
|
139
|
+
display: block;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
}
|
|
142
|
+
.content-wrapper {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-rows: 0fr;
|
|
145
|
+
transition: grid-template-rows ${this.animationTime}ms ${this.easing};
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
:host([open]) .content-wrapper {
|
|
150
|
+
grid-template-rows: 1fr;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.content-inner {
|
|
154
|
+
min-height: 0;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
<slot name="trigger"></slot>
|
|
158
|
+
<div class="content-wrapper">
|
|
159
|
+
<div class="content-inner">
|
|
160
|
+
<slot content></slot>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
static get observedAttributes() {
|
|
166
|
+
return ['open'];
|
|
167
|
+
}
|
|
168
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
169
|
+
if (oldValue === newValue)
|
|
170
|
+
return;
|
|
171
|
+
switch (name) {
|
|
172
|
+
case 'open':
|
|
173
|
+
const shouldBeOpen = newValue !== null;
|
|
174
|
+
// Only update if state actually changed to prevent infinite loop
|
|
175
|
+
if (this.open !== shouldBeOpen) {
|
|
176
|
+
this.open = shouldBeOpen;
|
|
177
|
+
this.updateTriggerAccessibility();
|
|
178
|
+
this.dispatchStateEvent();
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
connectedCallback() {
|
|
186
|
+
this.trigger = this.shadow.querySelector('slot[name="trigger"]');
|
|
187
|
+
if (this.trigger) {
|
|
188
|
+
this.trigger.addEventListener('click', this.handleTriggerClick);
|
|
189
|
+
// Setup accessibility for trigger element
|
|
190
|
+
requestAnimationFrame(() => {
|
|
191
|
+
this.setupTriggerAccessibility();
|
|
192
|
+
this.updateTriggerAccessibility();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
disconnectedCallback() {
|
|
197
|
+
if (this.trigger) {
|
|
198
|
+
this.trigger.removeEventListener('click', this.handleTriggerClick);
|
|
199
|
+
}
|
|
200
|
+
// Cleanup keyboard listener
|
|
201
|
+
if (this.triggerElement) {
|
|
202
|
+
this.triggerElement.removeEventListener('keydown', this.handleKeydown);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* AccordionGroup component - Container for managing multiple accordion items with mutual exclusion
|
|
208
|
+
*
|
|
209
|
+
* @element accordion-group
|
|
210
|
+
*
|
|
211
|
+
* @attr {boolean} allow-multiple-open - When present, allows multiple accordions to be open simultaneously. By default, opening one accordion closes all others in the group.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```html
|
|
215
|
+
* <!-- Only one accordion can be open at a time -->
|
|
216
|
+
* <accordion-group>
|
|
217
|
+
* <accordion-item open>
|
|
218
|
+
* <button slot="trigger">First Item</button>
|
|
219
|
+
* <div>Opening another will close this</div>
|
|
220
|
+
* </accordion-item>
|
|
221
|
+
*
|
|
222
|
+
* <accordion-item>
|
|
223
|
+
* <button slot="trigger">Second Item</button>
|
|
224
|
+
* <div>Only one can be open at a time</div>
|
|
225
|
+
* </accordion-item>
|
|
226
|
+
*
|
|
227
|
+
* <accordion-item>
|
|
228
|
+
* <button slot="trigger">Third Item</button>
|
|
229
|
+
* <div>Same behavior here</div>
|
|
230
|
+
* </accordion-item>
|
|
231
|
+
* </accordion-group>
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```html
|
|
236
|
+
* <!-- Allow multiple accordions to be open -->
|
|
237
|
+
* <accordion-group allow-multiple-open>
|
|
238
|
+
* <accordion-item>
|
|
239
|
+
* <button slot="trigger">First Item</button>
|
|
240
|
+
* <div>Can be open with others</div>
|
|
241
|
+
* </accordion-item>
|
|
242
|
+
*
|
|
243
|
+
* <accordion-item>
|
|
244
|
+
* <button slot="trigger">Second Item</button>
|
|
245
|
+
* <div>Multiple can be open</div>
|
|
246
|
+
* </accordion-item>
|
|
247
|
+
* </accordion-group>
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* @note Each accordion-group works independently. Multiple groups on the same page don't affect each other.
|
|
251
|
+
* @note The group listens to 'accordion-opened' events from child accordion-item elements
|
|
252
|
+
*/
|
|
253
|
+
export class AccordionGroup extends HTMLElement {
|
|
254
|
+
constructor() {
|
|
255
|
+
super();
|
|
256
|
+
this.handleAccordionOpened = (e) => {
|
|
257
|
+
if (this.allowMultiple)
|
|
258
|
+
return;
|
|
259
|
+
const childAccordions = this.querySelectorAll('accordion-item');
|
|
260
|
+
childAccordions.forEach(acc => {
|
|
261
|
+
const accordion = acc;
|
|
262
|
+
if (e.target !== accordion) {
|
|
263
|
+
if (accordion.open)
|
|
264
|
+
accordion.close();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
if (!AccordionGroup.stylesApplied) {
|
|
269
|
+
const sheet = new CSSStyleSheet();
|
|
270
|
+
sheet.replaceSync('accordion-group { display: block; }');
|
|
271
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
272
|
+
AccordionGroup.stylesApplied = true;
|
|
273
|
+
}
|
|
274
|
+
this.allowMultiple = this.hasAttribute('allow-multiple-open');
|
|
275
|
+
}
|
|
276
|
+
static get observedAttributes() {
|
|
277
|
+
return ['allow-multiple-open'];
|
|
278
|
+
}
|
|
279
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
280
|
+
if (oldValue === newValue)
|
|
281
|
+
return;
|
|
282
|
+
switch (name) {
|
|
283
|
+
case 'allow-multiple-open':
|
|
284
|
+
const shouldBeAllowed = newValue !== null;
|
|
285
|
+
// Only update if state actually changed to prevent infinite loop
|
|
286
|
+
if (this.allowMultiple !== shouldBeAllowed) {
|
|
287
|
+
this.allowMultiple = shouldBeAllowed;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
connectedCallback() {
|
|
295
|
+
this.addEventListener('accordion-opened', this.handleAccordionOpened);
|
|
296
|
+
}
|
|
297
|
+
disconnectedCallback() {
|
|
298
|
+
this.removeEventListener('accordion-opened', this.handleAccordionOpened);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
AccordionGroup.stylesApplied = false;
|
|
302
|
+
customElements.define('accordion-item', AccordionItem);
|
|
303
|
+
customElements.define('accordion-group', AccordionGroup);
|
|
304
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gnireeg/accordion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Accessible accordion web component with smooth animations, keyboard support, and nested accordion groups",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"accordion",
|
|
19
|
+
"web-components",
|
|
20
|
+
"custom-elements",
|
|
21
|
+
"accessible",
|
|
22
|
+
"a11y",
|
|
23
|
+
"typescript",
|
|
24
|
+
"ui-components"
|
|
25
|
+
],
|
|
26
|
+
"author": "Joel Geering <joel@geering.dev> (https://geering.dev)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/gnireeG/web-components.git",
|
|
31
|
+
"directory": "packages/accordion"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.4.0"
|
|
40
|
+
}
|
|
41
|
+
}
|