Skip to content

Commit ed7817e

Browse files
authored
Merge pull request #837 from Sysvale/feature/carousel-component
Feature/carousel component
2 parents e9e3f60 + 23ce438 commit ed7817e

File tree

6 files changed

+398
-1
lines changed

6 files changed

+398
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sysvale/cuida",
3-
"version": "3.87.0",
3+
"version": "3.88.0",
44
"description": "A design system built by Sysvale, using storybook and Vue components",
55
"repository": {
66
"type": "git",

src/components/Carousel.vue

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<!-- eslint-disable vue/multi-word-component-names -->
2+
<template>
3+
<div class="carousel-container">
4+
<div
5+
v-if="showArrows"
6+
class="carousel__arrow carousel__arrow--left"
7+
:class="{ 'carousel__arrow--dark': darkArrows }"
8+
@click="scrollToPrevious"
9+
>
10+
<icon
11+
name="arrow-left-outline"
12+
width="20"
13+
height="20"
14+
/>
15+
</div>
16+
17+
<div
18+
ref="carousel"
19+
class="carousel"
20+
@mousedown="startDrag"
21+
@mousemove="onDrag"
22+
@mouseup="stopDrag"
23+
@mouseleave="stopDrag"
24+
@touchstart="startDrag"
25+
@touchmove="onDrag"
26+
@touchend="stopDrag"
27+
>
28+
<div
29+
v-for="(item, index) in items"
30+
:key="item"
31+
class="carousel__item"
32+
>
33+
<!-- @slot Slot utilizado para renderização de cada item do carrossel. Os dados do escopo do slot podem ser acessados no formato a seguir: ```slot={ item, index }``` -->
34+
<slot
35+
:item="item"
36+
:index="index"
37+
name="default"
38+
/>
39+
</div>
40+
</div>
41+
42+
<div
43+
v-if="showArrows"
44+
class="carousel__arrow carousel__arrow--right"
45+
:class="{ 'carousel__arrow--dark': darkArrows }"
46+
@click="scrollToNext"
47+
>
48+
<icon
49+
name="arrow-right-outline"
50+
width="20"
51+
height="20"
52+
/>
53+
</div>
54+
</div>
55+
</template>
56+
57+
<script setup>
58+
import { ref, computed } from 'vue';
59+
import Icon from './Icon.vue';
60+
61+
const props = defineProps({
62+
/**
63+
* Array de itens a serem exibidos no carousel.
64+
*/
65+
items: {
66+
type: Array,
67+
default: () => [],
68+
},
69+
/**
70+
* Define a posição em que os itens serão alinhados quando for feita a ação de rolagem no carrossel.
71+
*/
72+
snapTo: {
73+
type: String,
74+
default: 'start',
75+
validator: (value) => ['start', 'center', 'end'].includes(value),
76+
},
77+
/**
78+
* Define o espaçamento entre os itens do carrossel.
79+
*/
80+
gap: {
81+
type: Number,
82+
default: 0,
83+
},
84+
/**
85+
* Controla a exibição das setas de rolagem.
86+
*/
87+
showArrows: {
88+
type: Boolean,
89+
default: false,
90+
},
91+
/**
92+
* Define se a cor das setas de rolagem deve ser escura.
93+
*/
94+
darkArrows: {
95+
type: Boolean,
96+
default: false,
97+
},
98+
});
99+
100+
const carousel = ref(null);
101+
let isDragging = false;
102+
let startX, scrollLeft;
103+
104+
const resolvedSnap = computed(() => props.snapTo);
105+
const resolvedGap = computed(() => `${props.gap * 4}px`);
106+
107+
function startDrag(event) {
108+
isDragging = true;
109+
startX = (event.pageX || event.touches[0].pageX) - carousel.value.offsetLeft;
110+
scrollLeft = carousel.value.scrollLeft;
111+
event.preventDefault();
112+
113+
carousel.value.style.scrollSnapType = 'none';
114+
}
115+
116+
function onDrag(event) {
117+
if (!isDragging) return;
118+
const x = (event.pageX || event.touches[0].pageX) - carousel.value.offsetLeft;
119+
const walk = (x - startX) * 1.5;
120+
carousel.value.scrollLeft = scrollLeft - walk;
121+
}
122+
123+
function stopDrag() {
124+
isDragging = false;
125+
126+
carousel.value.style.scrollSnapType = 'x mandatory';
127+
carousel.value.style.scrollBehavior = 'smooth';
128+
129+
setTimeout(() => {
130+
carousel.value.style.scrollBehavior = 'auto';
131+
}, 300);
132+
}
133+
134+
function scrollToNext() {
135+
const carouselElement = carousel.value;
136+
const itemWidth = carouselElement.querySelector('.carousel__item').offsetWidth + props.gap * 4;
137+
carouselElement.scrollBy({
138+
left: itemWidth,
139+
behavior: 'smooth',
140+
});
141+
}
142+
143+
function scrollToPrevious() {
144+
const carouselElement = carousel.value;
145+
const itemWidth = carouselElement.querySelector('.carousel__item').offsetWidth + props.gap * 4;
146+
carouselElement.scrollBy({
147+
left: -itemWidth,
148+
behavior: 'smooth',
149+
});
150+
}
151+
</script>
152+
153+
<style lang="scss" scoped>
154+
@import '../assets/sass/tokens.scss';
155+
156+
.carousel-container {
157+
position: relative;
158+
width: 100%;
159+
}
160+
161+
.carousel {
162+
display: flex;
163+
overflow-x: auto;
164+
gap: v-bind(resolvedGap);
165+
scroll-snap-type: x mandatory;
166+
scrollbar-width: none;
167+
-ms-overflow-style: none;
168+
cursor: grab;
169+
transition: scroll-snap-type 0.3s ease;
170+
171+
&::-webkit-scrollbar {
172+
display: none;
173+
}
174+
175+
&:active {
176+
cursor: grabbing;
177+
}
178+
179+
&__item {
180+
flex: none;
181+
scroll-snap-align: v-bind(resolvedSnap);
182+
}
183+
184+
&__arrow {
185+
position: absolute;
186+
top: 50%;
187+
transform: translateY(-50%);
188+
z-index: 1000;
189+
background-color: $n-0;
190+
color: $n-700;
191+
border-radius: 1000px;
192+
width: 40px;
193+
height: 40px;
194+
display: flex;
195+
justify-content: center;
196+
align-items: center;
197+
box-shadow: $shadow-md;
198+
cursor: pointer;
199+
200+
&--left {
201+
left: 0;
202+
margin: ml(3);
203+
}
204+
205+
&--right {
206+
right: 0;
207+
margin: mr(3);
208+
}
209+
210+
&--dark {
211+
background-color: $n-800;
212+
opacity: 0.85;
213+
color: $n-10;
214+
}
215+
}
216+
217+
}
218+
219+
</style>

src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Breadcrumb from './Breadcrumb.vue';
1818
import Button from './Button.vue';
1919
import Card from './Card.vue';
2020
import CalloutCard from './CalloutCard.vue';
21+
import Carousel from './Carousel.vue';
2122
import CarouselController from './CarouselController.vue';
2223
import Checkbox from './Checkbox.vue';
2324
import CheckboxGroup from './CheckboxGroup.vue';
@@ -136,6 +137,7 @@ export default {
136137
app.component('CdsButton', Button);
137138
app.component('CdsCard', Card);
138139
app.component('CdsCalloutCard', CalloutCard);
140+
app.component('CdsCarousel', Carousel);
139141
app.component('CdsCarouselController', CarouselController);
140142
app.component('CdsCheckbox', Checkbox);
141143
app.component('CdsCheckboxGroup', CheckboxGroup);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Meta, Story, Props, ArgsTable, Canvas, Preview } from '@storybook/addon-docs';
2+
import Carousel from '../../components/Carousel.vue';
3+
import Image from '../../components/Image.vue';
4+
5+
<Meta
6+
title="Componentes/Display/Carousel"
7+
component={ Carousel }
8+
argTypes={{
9+
snapTo: {
10+
control:{
11+
type: 'select',
12+
options: [
13+
'start',
14+
'center',
15+
'end',
16+
],
17+
}
18+
},
19+
}}
20+
parameters={{
21+
viewMode: 'docs',
22+
previewTabs: { canvas: { hidden: true }},
23+
docs: {
24+
source: {
25+
language: 'html',
26+
format:true,
27+
code: /*html*/
28+
`
29+
<cds-carousel
30+
v-slot="{ item }"
31+
:items="[
32+
'https://picsum.photos/600/800?random=1',
33+
'https://picsum.photos/600/800?random=2',
34+
'https://picsum.photos/600/800?random=3',
35+
'https://picsum.photos/600/800?random=4',
36+
]
37+
:gap="0"
38+
:snap-to="'start'"
39+
>
40+
<cds-image :src="item" width="300" height="400" />
41+
</cds-carousel>
42+
`
43+
},
44+
}
45+
}}
46+
/>
47+
48+
export const Template = (args) => ({
49+
components: { CdsCarousel: Carousel, CdsImage: Image },
50+
setup() {
51+
return { args };
52+
},
53+
template: /*html*/ `
54+
<cds-carousel
55+
v-bind="args"
56+
v-slot="{ item }"
57+
>
58+
<cds-image :src="item" width="300" height="400" />
59+
</cds-carousel>
60+
`,
61+
methods: {},
62+
});
63+
64+
# Carousel
65+
66+
### O Carousel é um componente que permite a exibição de uma série de conteúdos (imagens, textos, cards, etc.) em um formato deslizante, em que o usuário pode navegar entre os itens de forma sequencial.
67+
---
68+
<br />
69+
70+
## Quando usar:
71+
- Quando há necessidade de exibir vários itens (como imagens, produtos, cards informativos) em um espaço reduzido, sem sobrecarregar a interface.
72+
- Para destacar conteúdos importantes de forma dinâmica, como promoções, destaques ou novidades.
73+
- Quando a ordem de exibição dos itens é relevante e a navegação sequencial faz sentido para o contexto.
74+
75+
<br />
76+
77+
## Quando não usar:
78+
- Quando o conteúdo for uma informação crítica e não deve ficar escondida.
79+
- Se a quantidade de itens for muito pequena (menos de 3), pois a navegação pode parecer desnecessária.
80+
- Em interfaces onde a acessibilidade é uma prioridade e o carousel pode dificultar a experiência para usuários com deficiências visuais ou motoras.
81+
- Quando o espaço disponível na tela é insuficiente para exibir os itens de forma clara e legível.
82+
83+
<br />
84+
85+
## Obs:
86+
- Quando o carrossel for utilizado para exibir imagens, é recomendado usar o componente `Image` em vez da tag `<img>` nativa. Isso permite que todas as funcionalidades do <b>Image</b>, como <i>dimmed</i> e <i>opacity</i>, sejam utilizadas.
87+
88+
<br />
89+
90+
## Preview
91+
92+
<Canvas>
93+
<Story
94+
name="Carousel"
95+
args={{
96+
items: [
97+
'https://picsum.photos/600/800?random=1',
98+
'https://picsum.photos/600/800?random=2',
99+
'https://picsum.photos/600/800?random=3',
100+
'https://picsum.photos/600/800?random=4',
101+
'https://picsum.photos/600/800?random=5',
102+
'https://picsum.photos/600/800?random=6',
103+
'https://picsum.photos/600/800?random=7',
104+
],
105+
gap: 0,
106+
snapTo: 'start',
107+
showArrows: false,
108+
darkArrows: false,
109+
}}
110+
>
111+
{ Template.bind({}) }
112+
</Story>
113+
</Canvas>
114+
115+
<ArgsTable story="Carousel" />

src/tests/Carousel.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, test, expect } from 'vitest';
2+
import Carousel from '../components/Carousel.vue';
3+
import { mount } from '@vue/test-utils';
4+
5+
describe('Carousel', () => {
6+
test('renders correctly', async () => {
7+
const wrapper = mount(Carousel, {
8+
props: {
9+
items: [
10+
'https://picsum.photos/600/800?random=1',
11+
'https://picsum.photos/600/800?random=2',
12+
'https://picsum.photos/600/800?random=3',
13+
'https://picsum.photos/600/800?random=4',
14+
'https://picsum.photos/600/800?random=5',
15+
'https://picsum.photos/600/800?random=6',
16+
'https://picsum.photos/600/800?random=7',
17+
],
18+
gap: 0,
19+
snapTo: 'start',
20+
showArrows: false,
21+
darkArrows: false,
22+
},
23+
slots: {
24+
default: 'Texto',
25+
}
26+
});
27+
28+
expect(wrapper.html()).toMatchSnapshot();
29+
});
30+
});

0 commit comments

Comments
 (0)