Skip to content

Commit e316ae5

Browse files
committed
improve(accordion): fix ARIA violations, animation, stories, and tests
1 parent 1582864 commit e316ae5

5 files changed

Lines changed: 372 additions & 39 deletions

File tree

packages/core/custom-elements.json

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,44 @@
88
"declarations": [
99
{
1010
"kind": "class",
11-
"description": "An individual accordion item with a toggle header.",
11+
"description": "An individual accordion item with an animated toggle panel.",
1212
"name": "WuAccordionItem",
13-
"cssProperties": [],
14-
"cssParts": [],
13+
"cssProperties": [
14+
{
15+
"name": "--wu-accordion-header-bg",
16+
"description": "Header background colour"
17+
},
18+
{
19+
"name": "--wu-accordion-header-bg-hover",
20+
"description": "Header background on hover"
21+
},
22+
{
23+
"name": "--wu-accordion-header-color",
24+
"description": "Header text colour"
25+
},
26+
{
27+
"name": "--wu-accordion-padding-x",
28+
"description": "Horizontal padding"
29+
},
30+
{
31+
"name": "--wu-accordion-padding-y",
32+
"description": "Vertical padding"
33+
},
34+
{
35+
"name": "--wu-accordion-content-color",
36+
"description": "Body text colour"
37+
}
38+
],
39+
"cssParts": [
40+
{
41+
"name": "header",
42+
"description": "The trigger button"
43+
},
44+
{
45+
"name": "panel",
46+
"description": "The collapsible content panel"
47+
}
48+
],
1549
"slots": [
1650
{
1751
"name": "",
@@ -26,7 +60,7 @@
2660
"events": [
2761
{
2862
"name": "wu-toggle",
29-
"description": "Emitted when the item is opened or closed",
63+
"description": "Emitted when the item is opened or closed. Detail: `{ open: boolean }`",
3064
"type": {
3165
"text": "CustomEvent"
3266
}
@@ -66,9 +100,18 @@
66100
"declarations": [
67101
{
68102
"kind": "class",
69-
"description": "An accordion container. Optionally enforces single-open behaviour.",
103+
"description": "An accordion container that groups `<wu-accordion-item>` elements. Add the `single` attribute to restrict to one open item at a time.",
70104
"name": "WuAccordion",
71-
"cssProperties": [],
105+
"cssProperties": [
106+
{
107+
"name": "--wu-accordion-border",
108+
"description": "Border colour around the group"
109+
},
110+
{
111+
"name": "--wu-accordion-radius",
112+
"description": "Border radius of the group"
113+
}
114+
],
72115
"cssParts": [],
73116
"slots": [
74117
{

packages/core/src/components/accordion/wu-accordion.stories.ts

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,27 @@ const meta: Meta = {
55
title: 'Components/Accordion',
66
component: 'wu-accordion',
77
tags: ['autodocs'],
8+
argTypes: {
9+
single: {
10+
control: 'boolean',
11+
description: 'Restrict so only one item can be open at a time.',
12+
table: {
13+
type: { summary: 'boolean' },
14+
defaultValue: { summary: 'false' },
15+
},
16+
},
17+
},
18+
args: {
19+
single: false,
20+
},
821
parameters: {
922
docs: {
1023
description: {
11-
component: 'Collapsible disclosure panels. Set `multiple` to allow more than one panel open at a time. Compose with `<wu-accordion-item>` elements.',
24+
component:
25+
'Collapsible disclosure panels. Add the `single` attribute to restrict ' +
26+
'to one open item at a time. Compose with `<wu-accordion-item>` elements. ' +
27+
'Each item supports a `summary` slot (the clickable header) and a ' +
28+
'default slot (the revealed body content).',
1229
},
1330
},
1431
},
@@ -17,29 +34,35 @@ const meta: Meta = {
1734
export default meta;
1835
type Story = StoryObj;
1936

37+
/** Default accordion — multiple items can be open simultaneously. */
2038
export const Default: Story = {
21-
render: () => html`
22-
<wu-accordion style="max-width:560px">
39+
args: { single: false },
40+
render: (args) => html`
41+
<wu-accordion ?single=${args['single']} style="max-width:560px">
2342
<wu-accordion-item>
2443
<span slot="summary">What is WeldUI?</span>
25-
WeldUI is a framework-agnostic component library built with Lit Web Components.
26-
It works in any framework — React, Vue, Angular, Svelte, or plain HTML.
44+
WeldUI is a framework-agnostic component library built on Lit Web Components.
45+
It works in React, Vue, Angular, Svelte, or plain HTML with zero dependencies.
2746
</wu-accordion-item>
2847
<wu-accordion-item>
2948
<span slot="summary">How do I install it?</span>
3049
Run <code>pnpm add @weldui/core</code> and import the components you need.
50+
Each component auto-registers its custom element on import.
3151
</wu-accordion-item>
3252
<wu-accordion-item>
3353
<span slot="summary">Is it accessible?</span>
34-
Yes — all components target WCAG 2.1 AA compliance with full keyboard navigation.
54+
Yes — all components target WCAG 2.1 AA with full keyboard navigation,
55+
ARIA attributes, and focus management.
3556
</wu-accordion-item>
3657
</wu-accordion>
3758
`,
3859
};
3960

61+
/** With `single` — opening one item automatically closes the others. */
4062
export const SingleOpen: Story = {
41-
render: () => html`
42-
<wu-accordion single style="max-width:560px">
63+
args: { single: true },
64+
render: (args) => html`
65+
<wu-accordion ?single=${args['single']} style="max-width:560px">
4366
<wu-accordion-item open>
4467
<span slot="summary">First item (open by default)</span>
4568
This item is open by default. Opening another will close this one.
@@ -55,3 +78,61 @@ export const SingleOpen: Story = {
5578
</wu-accordion>
5679
`,
5780
};
81+
82+
/** Items can be individually disabled — they cannot be toggled by the user. */
83+
export const DisabledItems: Story = {
84+
render: () => html`
85+
<wu-accordion style="max-width:560px">
86+
<wu-accordion-item>
87+
<span slot="summary">Active item</span>
88+
This item is active and can be expanded freely.
89+
</wu-accordion-item>
90+
<wu-accordion-item disabled>
91+
<span slot="summary">Disabled item (cannot expand)</span>
92+
This content would never be revealed — the item is locked.
93+
</wu-accordion-item>
94+
<wu-accordion-item>
95+
<span slot="summary">Another active item</span>
96+
This item is also fully interactive.
97+
</wu-accordion-item>
98+
</wu-accordion>
99+
`,
100+
};
101+
102+
/** Body slots accept any HTML — code blocks, lists, inline elements. */
103+
export const RichContent: Story = {
104+
render: () => html`
105+
<wu-accordion style="max-width:560px">
106+
<wu-accordion-item open>
107+
<span slot="summary">Getting started</span>
108+
<p style="margin:0 0 var(--wu-space-3)">Install WeldUI with your package manager:</p>
109+
<pre style="
110+
margin:0;
111+
background:var(--wu-color-surface);
112+
padding:var(--wu-space-3) var(--wu-space-4);
113+
border-radius:var(--wu-radius-md);
114+
font-family:var(--wu-font-mono);
115+
font-size:var(--wu-text-xs);
116+
color:var(--wu-color-text);
117+
overflow-x:auto;
118+
">pnpm add @weldui/core</pre>
119+
</wu-accordion-item>
120+
<wu-accordion-item>
121+
<span slot="summary">Supported frameworks</span>
122+
<ul style="margin:0;padding-left:var(--wu-space-5);display:flex;flex-direction:column;gap:var(--wu-space-2)">
123+
<li>React — <code style="font-family:var(--wu-font-mono);font-size:var(--wu-text-xs)">@weldui/react</code></li>
124+
<li>Vue 3 — <code style="font-family:var(--wu-font-mono);font-size:var(--wu-text-xs)">@weldui/vue</code></li>
125+
<li>Angular — <code style="font-family:var(--wu-font-mono);font-size:var(--wu-text-xs)">@weldui/angular</code></li>
126+
<li>Svelte — native Web Components, no wrapper needed</li>
127+
<li>Plain HTML / JS — zero dependencies</li>
128+
</ul>
129+
</wu-accordion-item>
130+
<wu-accordion-item>
131+
<span slot="summary">Browser support</span>
132+
All evergreen browsers — Chrome, Firefox, Safari, and Edge (latest 2 versions).
133+
Web Components (Custom Elements v1 + Shadow DOM v1) are required.
134+
</wu-accordion-item>
135+
</wu-accordion>
136+
`,
137+
};
138+

packages/core/src/components/accordion/wu-accordion.styles.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const itemStyles = css`
3232
--wu-accordion-content-color: var(--wu-color-text-secondary);
3333
}
3434
35+
/* ── Trigger button ─────────────────────────────────── */
3536
.header {
3637
display: flex;
3738
align-items: center;
@@ -50,39 +51,64 @@ export const itemStyles = css`
5051
gap: var(--wu-space-4);
5152
}
5253
53-
.header:hover { background: var(--wu-accordion-header-bg-hover); }
54+
.header:hover:not(:disabled) { background: var(--wu-accordion-header-bg-hover); }
5455
5556
.header:focus-visible {
5657
outline: none;
5758
box-shadow: inset var(--wu-focus-ring);
5859
}
5960
61+
.header:disabled {
62+
opacity: 0.45;
63+
cursor: not-allowed;
64+
}
65+
66+
/* ── Chevron SVG ────────────────────────────────────── */
6067
.chevron {
6168
display: inline-flex;
6269
flex-shrink: 0;
63-
transition: transform var(--wu-duration-fast) var(--wu-ease-default);
6470
color: var(--wu-color-text-secondary);
71+
transition: transform var(--wu-duration-normal) var(--wu-ease-default);
6572
}
6673
6774
:host([open]) .chevron {
6875
transform: rotate(180deg);
6976
}
7077
78+
:host([disabled]) .chevron {
79+
opacity: 0.45;
80+
}
81+
82+
/* ── Panel — CSS grid height animation ──────────────── */
83+
/*
84+
* Using the grid 0fr → 1fr trick:
85+
* - .body transitions grid-template-rows for a smooth height animation
86+
* - .content has overflow:hidden so it clips while .body "grows"
87+
* - No fixed max-height needed — works for any content length
88+
*/
7189
.body {
72-
overflow: hidden;
73-
max-height: 0;
74-
transition: max-height var(--wu-duration-normal) var(--wu-ease-default);
90+
display: grid;
91+
grid-template-rows: 0fr;
92+
transition: grid-template-rows var(--wu-duration-normal) var(--wu-ease-default);
7593
}
7694
7795
:host([open]) .body {
78-
max-height: 1000px;
96+
grid-template-rows: 1fr;
7997
}
8098
8199
.content {
82-
padding: 0 var(--wu-accordion-padding-x) var(--wu-accordion-padding-y);
100+
overflow: hidden;
101+
padding: 0 var(--wu-accordion-padding-x);
83102
font-family: var(--wu-font-sans);
84103
font-size: var(--wu-text-sm);
85104
color: var(--wu-accordion-content-color);
86105
line-height: var(--wu-leading-normal);
106+
/* Animate padding so content doesn't jump at the end of the transition */
107+
transition: padding var(--wu-duration-normal) var(--wu-ease-default);
108+
}
109+
110+
:host([open]) .content {
111+
padding: 0 var(--wu-accordion-padding-x) var(--wu-accordion-padding-y);
87112
}
88113
`;
114+

0 commit comments

Comments
 (0)