Skip to content

Commit 3e4e64f

Browse files
Merge pull request #649 from thejackshelton/add-carousel
Add carousel component as draft
2 parents 72a36d9 + 58ed303 commit 3e4e64f

File tree

22 files changed

+819
-43
lines changed

22 files changed

+819
-43
lines changed

.changeset/short-dogs-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik-ui/headless': patch
3+
---
4+
5+
feat: carousel hits draft status!

apps/website/adapters/cloudflare-pages/vite.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { cloudflarePagesAdapter } from '@builder.io/qwik-city/adapters/cloudflar
22
import { extendConfig } from '@builder.io/qwik-city/vite';
33
import baseConfig from '../../vite.config';
44

5+
/**
6+
Vite 5.1.6 breaks the cloudflare build when deploying.
7+
This is a workaround until the issue is fixed.
8+
*/
9+
// @ts-expect-error
510
export default extendConfig(baseConfig, () => {
611
return {
712
build: {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const statusByComponent: ComponentKitsStatuses = {
3232
},
3333
headless: {
3434
Accordion: ComponentStatus.Beta,
35+
Carousel: ComponentStatus.Draft,
3536
Collapsible: ComponentStatus.Draft,
3637
Combobox: ComponentStatus.Beta,
3738
Modal: ComponentStatus.Beta,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.carousel {
2+
--slide-size: 100%;
3+
--slide-height: 5rem;
4+
height: 100%;
5+
max-width: 500px;
6+
overflow: hidden;
7+
}
8+
9+
.carousel-container {
10+
backface-visibility: hidden;
11+
display: flex;
12+
touch-action: pan-y;
13+
margin-left: calc(var(--slide-spacing) * -1);
14+
transition-property: transform;
15+
transition-timing-function: ease;
16+
overflow-x: visible;
17+
padding-block: 0.5rem;
18+
}
19+
20+
.carousel-slide {
21+
flex: 0 0 var(--slide-size);
22+
min-width: 0;
23+
position: relative;
24+
user-select: none;
25+
transition-property: transform;
26+
border: 2px dotted hsl(var(--primary));
27+
pointer-events: none;
28+
}
29+
30+
.carousel-pagination {
31+
display: flex;
32+
gap: 0.5rem;
33+
padding: 1rem;
34+
border: 2px dotted hsl(var(--foreground));
35+
}
36+
37+
.carousel-buttons {
38+
display: flex;
39+
justify-content: space-between;
40+
border: 2px dotted hsl(var(--accent));
41+
}
42+
43+
.carousel-buttons button[aria-disabled='true'] {
44+
opacity: 0.5;
45+
}
46+
47+
.carousel-buttons button {
48+
border: 2px dotted hsl(var(--foreground));
49+
padding: 0.5rem;
50+
}
51+
52+
.carousel-img {
53+
pointer-events: none;
54+
}
55+
56+
.carousel-pagination-bullet {
57+
cursor: pointer;
58+
padding-inline: 0.5rem;
59+
}
60+
61+
.pagination-underline {
62+
outline: 2px dotted hsl(var(--primary));
63+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
2+
3+
import {
4+
Carousel,
5+
CarouselNext,
6+
CarouselPrev,
7+
CarouselSlide,
8+
CarouselView,
9+
CarouselContainer,
10+
CarouselPagination,
11+
} from '@qwik-ui/headless';
12+
import carouselStyles from './carousel.css?inline';
13+
14+
export default component$(() => {
15+
/* TODO: document this to always have initial state to null.
16+
Use defaultSlide instead for setting a slide on page load */
17+
const currentIndexSig = useSignal<number>(0);
18+
useStyles$(carouselStyles);
19+
20+
const slideImageMetadata = [
21+
{
22+
id: '10',
23+
author: 'Paul Jarvis',
24+
width: 2500,
25+
height: 1667,
26+
url: 'https://unsplash.com/photos/6J--NXulQCs',
27+
download_url: 'https://picsum.photos/id/10/2500/1667',
28+
},
29+
{
30+
id: '11',
31+
author: 'Paul Jarvis',
32+
width: 2500,
33+
height: 1667,
34+
url: 'https://unsplash.com/photos/Cm7oKel-X2Q',
35+
download_url: 'https://picsum.photos/id/11/2500/1667',
36+
},
37+
{
38+
id: '12',
39+
author: 'Paul Jarvis',
40+
width: 2500,
41+
height: 1667,
42+
url: 'https://unsplash.com/photos/I_9ILwtsl_k',
43+
download_url: 'https://picsum.photos/id/12/2500/1667',
44+
},
45+
{
46+
id: '13',
47+
author: 'Paul Jarvis',
48+
width: 2500,
49+
height: 1667,
50+
url: 'https://unsplash.com/photos/3MtiSMdnoCo',
51+
download_url: 'https://picsum.photos/id/13/2500/1667',
52+
},
53+
{
54+
id: '14',
55+
author: 'Paul Jarvis',
56+
width: 2500,
57+
height: 1667,
58+
url: 'https://unsplash.com/photos/IQ1kOQTJrOQ',
59+
download_url: 'https://picsum.photos/id/14/2500/1667',
60+
},
61+
{
62+
id: '15',
63+
author: 'Paul Jarvis',
64+
width: 2500,
65+
height: 1667,
66+
url: 'https://unsplash.com/photos/NYDo21ssGao',
67+
download_url: 'https://picsum.photos/id/15/2500/1667',
68+
},
69+
{
70+
id: '16',
71+
author: 'Paul Jarvis',
72+
width: 2500,
73+
height: 1667,
74+
url: 'https://unsplash.com/photos/gkT4FfgHO5o',
75+
download_url: 'https://picsum.photos/id/16/2500/1667',
76+
},
77+
{
78+
id: '17',
79+
author: 'Paul Jarvis',
80+
width: 2500,
81+
height: 1667,
82+
url: 'https://unsplash.com/photos/Ven2CV8IJ5A',
83+
download_url: 'https://picsum.photos/id/17/2500/1667',
84+
},
85+
{
86+
id: '18',
87+
author: 'Paul Jarvis',
88+
width: 2500,
89+
height: 1667,
90+
url: 'https://unsplash.com/photos/Ps2n0rShqaM',
91+
download_url: 'https://picsum.photos/id/18/2500/1667',
92+
},
93+
{
94+
id: '19',
95+
author: 'Paul Jarvis',
96+
width: 2500,
97+
height: 1667,
98+
url: 'https://unsplash.com/photos/P7Lh0usGcuk',
99+
download_url: 'https://picsum.photos/id/19/2500/1667',
100+
},
101+
];
102+
103+
return (
104+
<Carousel
105+
bind:currSlideIndex={currentIndexSig}
106+
spaceBetweenSlides={30}
107+
class="carousel"
108+
>
109+
<div class="carousel-buttons">
110+
<CarouselPrev class="prev-button">Prev</CarouselPrev>
111+
<CarouselNext class="next-button">Next</CarouselNext>
112+
</div>
113+
<CarouselView>
114+
<CarouselContainer class="carousel-container">
115+
{slideImageMetadata.map((data) => (
116+
<CarouselSlide key={data.id} class="carousel-slide">
117+
<img
118+
class="carousel-img"
119+
width="640"
120+
height="320"
121+
src={`https://picsum.photos/id/${data.id}/640/320`}
122+
alt={data.author}
123+
/>
124+
</CarouselSlide>
125+
))}
126+
</CarouselContainer>
127+
</CarouselView>
128+
<div>
129+
<CarouselPagination
130+
class="carousel-pagination"
131+
renderBullet$={$((i: number) => {
132+
return (
133+
<div
134+
class={`carousel-pagination-bullet ${
135+
currentIndexSig.value === i ? 'pagination-underline' : ''
136+
}`}
137+
onClick$={() => (currentIndexSig.value = i)}
138+
>
139+
{i}
140+
</div>
141+
);
142+
})}
143+
/>
144+
</div>
145+
</Carousel>
146+
);
147+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.Carousel} />
6+
7+
# Carousel
8+
9+
A control that displays a set of content in a scrolling container.
10+
11+
<Showcase name="hero" />
12+
13+
## ✨ Features
14+
15+
<FeatureList
16+
features={[
17+
'Mobile Swiping',
18+
'Draggable',
19+
'Dynamic Slide Offsetting',
20+
'Transition Duration Control',
21+
'Pagination',
22+
'Navigate via Prev/Next buttons',
23+
]}
24+
roadmap={[
25+
'Tests',
26+
'Documentation',
27+
'Fix for the "snapping" behavior when going pass the last slide',
28+
'A refactor of the API (for example, conventions like the select)',
29+
'Remove additional styles to the headless component',
30+
]}
31+
/>
32+
33+
> Want to speed up the process of getting this component ready for production? Check out the [contributing guide](/docs/headless/contributing)!
34+
35+
### What's different to Swiper JS or Embla Carousel?
36+
37+
This component initially started as a wrapper around Swiper JS, but has since been rewritten natively in Qwik! It's a automatically optimized and a lightweight solution.
38+
39+
This component also intends to be more accessible with a powerful API.
40+
41+
### Why is there styles currently directly in headless?
42+
43+
The component itself does include headless logic, but also uses the flex layout algorithm, and is in sort of a "gray" zone betweeen styled / unstyled.
44+
45+
In the future, we would prefer a polished headless component (devoid of styles for most part).

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+
- [Carousel](/docs/headless/carousel)
1213
- [Collapsible](/docs/headless/collapsible)
1314
- [Combobox](/docs/headless/combobox)
1415
- [Modal](/docs/headless/modal)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@
194194
"unified": "^11.0.4",
195195
"unist-util-visit": "^5.0.0",
196196
"verdaccio": "^5.0.4",
197-
"vite": "^5.1.6",
197+
"vite": "5.0.4",
198198
"vite-plugin-dts": "^3.5.3",
199199
"vite-plugin-eslint": "^1.8.1",
200200
"vite-plugin-inspect": "0.7.38",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { component$, type PropsOf, Slot, useContext } from '@builder.io/qwik';
2+
import CarouselContextId from './carousel-context-id';
3+
4+
type CarouselContainerProps = PropsOf<'div'>;
5+
6+
export const CarouselContainer = component$((props: CarouselContainerProps) => {
7+
const context = useContext(CarouselContextId);
8+
9+
return (
10+
<div
11+
ref={context.containerRef}
12+
style={{
13+
transform: `translate3d(${context.slideOffsetSig.value}px, 0px, 0px)`,
14+
transitionDuration: `${context.transitionDurationSig.value}ms`,
15+
transitionDelay: '0ms',
16+
}}
17+
{...props}
18+
>
19+
<Slot />
20+
</div>
21+
);
22+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createContextId } from '@builder.io/qwik';
2+
import { type CarouselContext } from './carousel-context.type';
3+
4+
const CarouselContextId = createContextId<CarouselContext>('carousel-context');
5+
6+
export default CarouselContextId;

0 commit comments

Comments
 (0)