Skip to content

Commit b35c4c2

Browse files
Merge pull request #587 from thejackshelton/feat/collapsible
feat(collapsible): new collapsible component
2 parents fa7cd97 + 0166e80 commit b35c4c2

File tree

20 files changed

+491
-67
lines changed

20 files changed

+491
-67
lines changed

apps/website/src/_state/component-statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const statusByComponent: ComponentKitsStatuses = {
4242
headless: {
4343
Accordion: ComponentStatus.Beta,
4444
Carousel: ComponentStatus.Planned,
45+
Collapsible: ComponentStatus.Draft,
4546
Combobox: ComponentStatus.Beta,
4647
Dialog: ComponentStatus.Planned,
4748
Modal: ComponentStatus.Beta,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { QwikIntrinsicElements } from '@builder.io/qwik';
2+
3+
export function DoubleChevron(props: QwikIntrinsicElements['svg'], key: string) {
4+
return (
5+
<svg
6+
xmlns="http://www.w3.org/2000/svg"
7+
width="1em"
8+
height="1em"
9+
viewBox="0 0 16 16"
10+
{...props}
11+
key={key}
12+
>
13+
<path
14+
fill="currentColor"
15+
fill-rule="evenodd"
16+
d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708m0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708"
17+
></path>
18+
</svg>
19+
);
20+
}

apps/website/src/routes/docs/headless/collapse/index.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@keyframes collapsible-open {
2+
0% {
3+
height: 0;
4+
}
5+
100% {
6+
height: var(--qwikui-collapsible-content-height);
7+
}
8+
}
9+
10+
@keyframes collapsible-closed {
11+
0% {
12+
height: var(--qwikui-collapsible-content-height);
13+
}
14+
100% {
15+
height: 0;
16+
}
17+
}
18+
19+
.animation[data-state='open'] {
20+
animation: 550ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-open;
21+
}
22+
23+
.animation[data-state='closed'] {
24+
animation: 350ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-closed;
25+
}
26+
27+
.animation {
28+
overflow: hidden;
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@qwik-ui/headless';
3+
import { DoubleChevron } from '~/components/icons/double-chevron';
4+
import './collapsible.css';
5+
6+
export default component$(() => {
7+
return (
8+
<Collapsible class="flex flex-col">
9+
<div class="mb-2 flex items-center justify-between px-2">
10+
<span class="w-fit">
11+
<strong>@thejackshelton</strong> starred 3 repositories
12+
</span>
13+
<CollapsibleTrigger class="shadow-light-medium dark:shadow-dark-medium mx-2 mr-0 rounded-md bg-slate-700 p-1 text-white dark:bg-slate-800">
14+
<DoubleChevron class="size-4" />
15+
</CollapsibleTrigger>
16+
</div>
17+
<div class="shadow-dark-low dark:shadow-dark-medium mx-2 mb-2 rounded-md bg-slate-700 p-2 text-white dark:bg-slate-800">
18+
@qwik-ui/headless
19+
</div>
20+
<CollapsibleContent class="animation">
21+
<div class="shadow-dark-low dark:shadow-dark-medium mx-2 mb-2 rounded-md bg-slate-700 p-2 text-white dark:bg-slate-800">
22+
@builder.io/qwik
23+
</div>
24+
<div class="shadow-dark-low dark:shadow-dark-medium mx-2 rounded-md bg-slate-700 p-2 text-white dark:bg-slate-800">
25+
@qwikdev/astro
26+
</div>
27+
<div class="p-2"></div>
28+
</CollapsibleContent>
29+
</Collapsible>
30+
);
31+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@qwik-ui/headless';
3+
import './collapsible.css';
4+
5+
export default component$(() => {
6+
return (
7+
<div>
8+
<Collapsible>
9+
<CollapsibleTrigger>Without animation</CollapsibleTrigger>
10+
<CollapsibleContent>I am the content 2!</CollapsibleContent>
11+
</Collapsible>
12+
</div>
13+
);
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { component$, useSignal } from '@builder.io/qwik';
2+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@qwik-ui/headless';
3+
import './collapsible.css';
4+
5+
export default component$(() => {
6+
const openSig = useSignal<boolean>(true);
7+
8+
return (
9+
<Collapsible bind:isOpen={openSig}>
10+
<CollapsibleTrigger>I am trigger 1!</CollapsibleTrigger>
11+
<CollapsibleContent class="animation p-4">I am the content 1!</CollapsibleContent>
12+
</Collapsible>
13+
);
14+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { component$, useSignal } from '@builder.io/qwik';
2+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@qwik-ui/headless';
3+
import './collapsible.css';
4+
5+
export default component$(() => {
6+
const openSig = useSignal<boolean>(true);
7+
8+
return (
9+
<div>
10+
<p>state: {openSig.value ? 'true' : 'false'}</p>
11+
<Collapsible bind:isOpen={openSig}>
12+
<CollapsibleTrigger>I am trigger 1!</CollapsibleTrigger>
13+
<CollapsibleContent class="animation">I am the content 1!</CollapsibleContent>
14+
</Collapsible>
15+
<button
16+
class="rounded-md bg-slate-500 px-2 py-3 text-white"
17+
onClick$={() => {
18+
openSig.value = !openSig.value;
19+
}}
20+
>
21+
Programmatic toggle
22+
</button>
23+
</div>
24+
);
25+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { statusByComponent } from '~/_state/component-statuses';
2+
import { FeatureList } from '~/components/feature-list/feature-list';
3+
import { Note } from '~/components/note/note';
4+
5+
<StatusBanner status={statusByComponent.headless.Accordion} />
6+
7+
# Collapsible
8+
9+
An interactive component which expands/collapses a panel.
10+
11+
<Showcase name="hero" />
12+
13+
Follows the WAI-ARIA disclosure pattern.
14+
15+
<FeatureList
16+
features={[
17+
'Can open by default on SSR (without waking up the component!)',
18+
'Does not execute until interaction (including programmatically)',
19+
'Does not use any visible tasks',
20+
'Automatic animation detection (deprecating the `animated` prop)',
21+
`Provides a bind:isOpen prop so that users can open or close it however they'd like (controlled)`,
22+
'A `defaultOpen` prop to open it by default (uncontrolled)',
23+
'Style based on data states. `initial`, `open`, or `closed`',
24+
]}
25+
/>
26+
27+
## Jack's bug watch 🔎:
28+
29+
<strong style={{ color: 'red' }}>BUG:</strong> Refresh the page and click on programmatic toggle.
30+
The animation does not work.
31+
32+
<Showcase name="programmatic" />
33+
34+
<strong style={{ color: 'red' }}>BUG:</strong> resize the window after closing and opening
35+
the collapsible. (content height does not resize)
36+
37+
<Showcase name="no-animation" />
38+
39+
<strong style={{ color: 'red' }}>BUG:</strong> watch the animation. Issue explained further
40+
below.
41+
42+
<Showcase name="padding" />
43+
44+
## Hey Qwik developers!
45+
46+
Qwik UI is already on the path to a library that gives you the features you know and love with other headless libraries (hopefully more functionality), with the performance of _just_ grabbing html on the page.
47+
48+
To work towards this, I have been working on a new primitive to rebuild the accordion from the ground up. (which currently uses visible tasks)
49+
50+
<Note status="warning">
51+
Unfortunately, I don't have the time right now to complete it, but we are _basically
52+
80%_ there. Would really appreciate some help on this. It still needs tests (many of
53+
which can be taken from the accordion).
54+
</Note>
55+
56+
It is a collapsible or [disclosure component](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/), similar to native html's `details & summary` tags. Unfortunately, the accessibility for these native elements is very flaky, and thus, this component is needed.
57+
58+
Not only does it benefit this component, but also the performance of the accordion (which would be built on top of this).
59+
60+
### What I'd like to add:
61+
62+
<FeatureList
63+
features={[
64+
'An `onOpenChange$` prop function that runs when the collapsible has opened or closed',
65+
]}
66+
/>
67+
68+
## There are two minor bugs:
69+
70+
### **BUG #1:**
71+
72+
We currently detect automatic animations with the `getComputedStyle` API. Unfortunately, this is on the client, and so we do not get an animation if:
73+
74+
- was loaded on SSR **AND** open by default
75+
76+
I was however, able to make it work when clicking the actual button's trigger with a 1ms setTimeout inside the `handleClick$`, but it is still a problem with programmatic behavior.
77+
78+
We need to figure out how to detect the animation with SSR **OR** use a different way of animating. I briefly tried using `requestAnimationFrame` as an alternative.
79+
80+
> My current thoughts are, setting it as an animated collapsible on the server, but then setting it back to false on the client when resumed if it does not have an animation (the current logic for that does not work)
81+
82+
[where the issue is](https://github.com/thejackshelton/collapsible/blob/main/src/components/collapsible/collapsible-content.tsx#L27)
83+
84+
### **BUG #2:**
85+
86+
If you close and open the collapsible again, try resizing the window, notice that the container does not adjust its height to match its content.
87+
88+
[where the issue is](https://github.com/thejackshelton/collapsible/blob/main/src/components/collapsible/collapsible-trigger.tsx#L16)
89+
90+
> You can see visuals of the problems [on discord here.](https://discord.com/channels/990511757091033108/1040763063533588562/1193628593088106496)
91+
92+
The overall component is relatively small, around 250 lines of code.
93+
94+
### **BUG #3:**
95+
96+
Padding currently breaks height animations. This is because the padding is applied to the content, and the content is animated. This causes the content to animate from the padding, which is not the desired effect. :facepalm

apps/website/src/routes/docs/headless/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
## Components
1010

1111
- [Accordion](/docs/headless/accordion)
12+
- [Collapsible](/docs/headless/collapsible)
1213
- [Combobox](/docs/headless/combobox)
1314
- [Modal](/docs/headless/modal)
1415
- [Pagination](/docs/headless/pagination)

0 commit comments

Comments
 (0)