Skip to content

Commit c8b34c9

Browse files
authored
fix: improve animation handling & collapsible story (#11)
* fix: streamline animation handling and remove unused transition states * fix: remove unused 'as' prop from CollapsibleRoot component * fix: enhance accessibility and interaction handling in CollapsibleBond header * fix: replace GSAP with motion for animation handling in Collapsible stories * fix: remove debug console log from accordion.item.header preset * fix: remove dynamic state display from Collapsible header * fix: adjust padding for collapsible components for better spacing * fix: update composable section header emoji for consistency * bump version to 1.0.0-alpha.21 * fix: update dev&build commands * fix: remove npm commands
1 parent b85513e commit c8b34c9

File tree

7 files changed

+107
-112
lines changed

7 files changed

+107
-112
lines changed

README.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Leverages Svelte's fine-grained reactivity system for optimal performance and sm
4040

4141
Components are headless by default, giving you complete control over styling while providing sensible defaults.
4242

43-
### 🎨 **Composable**
43+
### 🧩 **Composable**
4444

4545
Build complex UIs by combining simple, reusable components. Each component is designed to work seamlessly with others through the Bond pattern and context API. Create sophisticated features like multi-level dropdowns, nested accordions, or custom form controls by composing atomic components together.
4646

@@ -522,23 +522,17 @@ This example demonstrates the power of component composition by combining `Dropd
522522

523523
```bash
524524
bun install
525-
# or
526-
npm install
527525
```
528526

529527
3. **Start development server:**
530528

531529
```bash
532530
bun dev
533-
# or
534-
npm run dev
535531
```
536532

537533
4. **Run Storybook:**
538534
```bash
539-
bun storybook
540-
# or
541-
npm run storybook
535+
bun run storybook:dev
542536
```
543537

544538
### Building
@@ -548,7 +542,7 @@ This example demonstrates the power of component composition by combining `Dropd
548542
bun run build
549543

550544
# Build Storybook
551-
bun run build-storybook
545+
bun run storybook:build
552546
```
553547

554548
---

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@svelte-atoms/core",
3-
"version": "1.0.0-alpha.20",
3+
"version": "1.0.0-alpha.21",
44
"description": "A modular, accessible, and extensible Svelte UI component library.",
55
"repository": {
66
"type": "git",

src/lib/components/collapsible/bond.svelte.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,31 @@ export class CollapsibleBond extends Bond<
5050
}
5151

5252
header(props: Record<string, unknown> = {}) {
53+
const isDisabled = this.state?.props?.disabled ?? false;
54+
const isOpen = this.state?.props?.open ?? false;
55+
const isButton = this.elements.header instanceof HTMLButtonElement;
56+
5357
return {
54-
'aria-disabled': this.state?.props?.disabled ?? false,
55-
'aria-expanded': this.state?.props?.open ?? false,
58+
'aria-disabled': isDisabled ? 'true' : 'false',
59+
'aria-expanded': isOpen ? 'true' : 'false',
5660
'aria-controls': `collapsible-body-${this.id}`,
57-
disabled: this.elements.header instanceof HTMLButtonElement ? this.state?.props.disabled : '',
58-
role: this.elements.header instanceof HTMLButtonElement ? undefined : 'button',
59-
tabindex: this.state?.props?.open ? 0 : -1, // Make focusable if open
61+
disabled: isButton ? isDisabled : undefined,
62+
role: isButton ? undefined : 'button',
63+
tabindex: isButton ? undefined : isDisabled ? -1 : 0,
6064
id: `collapsible-header-${this.id}`,
6165
'data-atom': this.id ?? '',
6266
'data-kind': 'collapsible-header',
6367
...props,
6468
onclick: () => {
65-
this.state.toggle();
69+
if (!isDisabled) {
70+
this.state.toggle();
71+
}
72+
},
73+
onkeydown: (e: KeyboardEvent) => {
74+
if (!isDisabled && (e.key === 'Enter' || e.key === ' ')) {
75+
e.preventDefault();
76+
this.state.toggle();
77+
}
6678
},
6779
[createAttachmentKey()]: (node: HTMLElement) => {
6880
this.elements.header = node;
@@ -71,13 +83,15 @@ export class CollapsibleBond extends Bond<
7183
}
7284

7385
body(props: Record<string, unknown> = {}) {
86+
const isOpen = this.state?.props?.open ?? false;
87+
7488
return {
7589
'aria-labelledby': `collapsible-header-${this.id}`,
76-
'aria-hidden': !this.state?.props?.open, // Hide when closed
77-
role: 'region', // Announce as a region
90+
role: 'region',
7891
id: `collapsible-body-${this.id}`,
7992
'data-atom': this.id ?? '',
8093
'data-kind': 'collapsible-body',
94+
inert: isOpen ? undefined : true,
8195
...props,
8296
[createAttachmentKey()]: (node: HTMLElement) => {
8397
this.elements.body = node;

src/lib/components/collapsible/collapsible-root.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
value = nanoid(),
3131
data = undefined,
3232
disabled = false,
33-
as = 'div' as E,
3433
factory = _factory,
3534
children = undefined,
3635
onmount = undefined,
@@ -71,16 +70,15 @@
7170
</script>
7271

7372
<HtmlAtom
73+
{bond}
7474
preset="collapsible"
7575
class={['flex w-full flex-col overflow-hidden', '$preset', klass]}
76-
{bond}
7776
onmount={onmount?.bind(bond.state)}
7877
ondestroy={ondestroy?.bind(bond.state)}
7978
animate={animate?.bind(bond.state)}
8079
enter={enter?.bind(bond.state)}
8180
exit={exit?.bind(bond.state)}
8281
initial={initial?.bind(bond.state)}
83-
{as}
8482
{...rootProps}
8583
>
8684
{@render children?.({ collapsible: bond })}

src/lib/components/collapsible/collapsible.stories.svelte

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script module>
22
import { defineMeta } from '@storybook/addon-svelte-csf';
33
import { Collapsible as ACollapsible } from '.';
4-
import { Root as CoolapsibleRoot } from './atoms';
54
import Root from '$svelte-atoms/core/components/root/root.svelte';
6-
import gsap from 'gsap';
7-
import { linear } from 'svelte/easing';
8-
import { toTransitionConfig } from '$svelte-atoms/core/utils/gsap';
5+
6+
import { animate as motion } from 'motion';
97
108
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
119
const { Story } = defineMeta({
@@ -35,33 +33,34 @@
3533
</ACollapsible.Header>
3634

3735
<ACollapsible.Body
38-
initial={(node) => {
39-
gsap.set(node, {
40-
opacity: 0,
41-
height: 0,
42-
pointerEvents: isOpen ? 'all' : 'none'
43-
});
44-
}}
36+
class={['pointer-events-none h-0 opacity-0', isOpen && 'pointer-events-auto']}
4537
enter={(node) => {
46-
const tween = gsap.to(node, {
47-
opacity: +isOpen,
48-
height: isOpen ? 'auto' : 0,
49-
duration: 0.2,
50-
ease: linear
51-
});
52-
return toTransitionConfig(tween);
38+
motion(
39+
node,
40+
{
41+
opacity: +isOpen,
42+
height: isOpen ? 'auto' : 0
43+
},
44+
{
45+
duration: 0.2,
46+
ease: 'linear'
47+
}
48+
);
49+
return { duration: 0.2 };
5350
}}
5451
exit={(node) => {
55-
const tween = gsap.to(node, { opacity: 0, height: 0, duration: 0.2, ease: linear });
56-
return toTransitionConfig(tween);
52+
motion(node, { opacity: 0, height: 0 }, { duration: 0.2, ease: 'linear' });
53+
return { duration: 0.2 };
5754
}}
5855
animate={(node) => {
59-
gsap.to(node, {
60-
opacity: +isOpen,
61-
height: isOpen ? 'auto' : 0,
62-
pointerEvents: isOpen ? 'all' : 'none',
63-
duration: 0.1
64-
});
56+
motion(
57+
node,
58+
{
59+
opacity: +isOpen,
60+
height: isOpen ? 'auto' : 0
61+
},
62+
{ duration: 0.2, ease: 'linear' }
63+
);
6564
}}
6665
>
6766
<div class="py-2">
@@ -83,35 +82,34 @@
8382
</ACollapsible.Header>
8483

8584
<ACollapsible.Body
86-
initial={(node) => {
87-
gsap.set(node, {
88-
opacity: 0,
89-
height: 0,
90-
pointerEvents: isOpen ? 'all' : 'none'
91-
});
92-
}}
85+
class={['pointer-events-none h-0 opacity-0', isOpen && 'pointer-events-auto']}
9386
enter={(node) => {
94-
const tween = gsap.to(node, {
95-
opacity: +isOpen,
96-
height: isOpen ? 'auto' : 0,
97-
duration: 0.2,
98-
ease: linear
99-
});
100-
101-
return toTransitionConfig(tween);
87+
motion(
88+
node,
89+
{
90+
opacity: +isOpen,
91+
height: isOpen ? 'auto' : 0
92+
},
93+
{
94+
duration: 0.2,
95+
ease: 'linear'
96+
}
97+
);
98+
return { duration: 0.2 };
10299
}}
103100
exit={(node) => {
104-
const tween = gsap.to(node, { opacity: 0, height: 0, duration: 0.2, ease: linear });
105-
106-
return toTransitionConfig(tween);
101+
motion(node, { opacity: 0, height: 0 }, { duration: 0.2, ease: 'linear' });
102+
return { duration: 0.2 };
107103
}}
108104
animate={(node) => {
109-
gsap.to(node, {
110-
opacity: +isOpen,
111-
height: isOpen ? 'auto' : 0,
112-
pointerEvents: isOpen ? 'all' : 'none',
113-
duration: 0.1
114-
});
105+
motion(
106+
node,
107+
{
108+
opacity: +isOpen,
109+
height: isOpen ? 'auto' : 0
110+
},
111+
{ duration: 0.1, ease: 'linear' }
112+
);
115113
}}
116114
>
117115
<div class="py-2">
@@ -133,34 +131,34 @@
133131
</ACollapsible.Header>
134132

135133
<ACollapsible.Body
136-
initial={(node) => {
137-
gsap.set(node, {
138-
opacity: +isOpen,
139-
height: isOpen ? 'auto' : 0,
140-
pointerEvents: isOpen ? 'all' : 'none'
141-
});
142-
}}
134+
class={['pointer-events-none h-0 opacity-0', isOpen && 'pointer-events-auto']}
143135
enter={(node) => {
144-
const tween = gsap.to(node, {
145-
opacity: +isOpen,
146-
height: 'auto',
147-
duration: 0.2,
148-
ease: linear
149-
});
150-
151-
return toTransitionConfig(tween);
136+
motion(
137+
node,
138+
{
139+
opacity: +isOpen,
140+
height: isOpen ? 'auto' : 0
141+
},
142+
{
143+
duration: 0.2,
144+
ease: 'linear'
145+
}
146+
);
147+
return { duration: 0.2 };
152148
}}
153149
exit={(node) => {
154-
const tween = gsap.to(node, { opacity: 0, height: 0, duration: 0.2, ease: linear });
155-
return toTransitionConfig(tween);
150+
motion(node, { opacity: 0, height: 0 }, { duration: 0.2, ease: 'linear' });
151+
return { duration: 0.2 };
156152
}}
157153
animate={(node) => {
158-
gsap.to(node, {
159-
opacity: +isOpen,
160-
height: isOpen ? 'auto' : 0,
161-
pointerEvents: isOpen ? 'all' : 'none',
162-
duration: 0.1
163-
});
154+
motion(
155+
node,
156+
{
157+
opacity: +isOpen,
158+
height: isOpen ? 'auto' : 0
159+
},
160+
{ duration: 0.1, ease: 'linear' }
161+
);
164162
}}
165163
>
166164
<div class="py-2">

src/lib/components/element/html-element.svelte

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,46 +23,38 @@
2323
2424
let node = $state<Element>();
2525
26-
let hasIntroTransitionStarted: boolean | undefined = $state(undefined);
27-
let skipFirstAnimate = true;
26+
let skipFirstAnimate = $state(!!enter);
2827
2928
$effect(() => {
3029
if (!node) return;
3130
3231
const unmount = untrack(() => onmount?.(node!));
3332
34-
if (!enter || typeof hasIntroTransitionStarted === 'undefined') {
35-
skipFirstAnimate = false;
36-
}
37-
3833
return () => {
3934
if (typeof unmount === 'function') unmount(node!);
4035
ondestroy?.(node!);
4136
};
4237
});
4338
4439
$effect(() => {
40+
const fn = animate;
41+
4542
if (!node) return;
43+
const shouldSkip = untrack(() => skipFirstAnimate);
4644
47-
if (skipFirstAnimate) {
45+
if (shouldSkip) {
4846
skipFirstAnimate = false;
4947
return;
5048
}
5149
52-
animate?.(node);
50+
fn?.(node);
5351
});
5452
5553
const elementProps = $derived({
5654
[createAttachmentKey()]: (n: Element) => {
5755
node = n;
5856
},
5957
class: cn(toClassValue(klass)),
60-
onintrostart: () => {
61-
hasIntroTransitionStarted = true;
62-
},
63-
onintroend: () => {
64-
hasIntroTransitionStarted = false;
65-
},
6658
...restProps
6759
});
6860

src/stories/Theme.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
class: 'mb-2 last:mb-0 rounded-md border border-border bg-popover px-2 py-2'
2020
}),
2121
'accordion.item.header': (bond) => {
22-
console.log('bond in preset:', bond);
2322
return defineState([
2423
defineProperty('class', () => [
2524
bond?.state?.isActive ? 'text-foreground/100' : 'text-foreground/50'
@@ -39,13 +38,13 @@
3938
class: 'pr-2 pl-4'
4039
}),
4140
collapsible: () => ({
42-
class: 'max-w-md rounded-md border border-border'
41+
class: 'max-w-md rounded-md border border-border p-2'
4342
}),
4443
'collapsible.header': () => ({
45-
class: 'px-4 py-2 hover:bg-foreground/5 active:bg-foreground/10 flex cursor-pointer'
44+
class: 'px-2 py-2 hover:bg-foreground/5 active:bg-foreground/10 flex cursor-pointer rounded'
4645
}),
4746
'collapsible.body': () => ({
48-
class: 'text-sm px-4'
47+
class: 'text-sm px-2'
4948
}),
5049
'popover.content': () => ({
5150
class: ''

0 commit comments

Comments
 (0)