From 61aaa7f75f7d9599645f0c40d1dfded39d7014d0 Mon Sep 17 00:00:00 2001 From: Matheus Domingos <134434652+DominMFD@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:48:04 -0300 Subject: [PATCH] Sliding Component (#209) * feat: sliding component create * fix: rename parameter in add and remove functions * chore: init refactor sliding component * refactor: refactor sliding component * chore: implements error handling in remove method * chore: add controls in storybook * fix: delete some blank lines * chore: change slide css and remove updateWidth function * refactor: refactor testes * refactor: refactor sliding tests * test: adjusted the sliding tests * test: sliding tests fixed * test: adjust sliding unmount test * chore: remove this.slides and add activeSlide method * fix: create swipeLet and swipeRight methods --------- Co-authored-by: Alexandre Gomes --- src/components/Sliding/index.js | 108 +++++++++++++++++++++++ src/components/Sliding/index.scss | 20 +++++ src/components/Sliding/index.spec.js | 127 +++++++++++++++++++++++++++ src/stories/Sliding.stories.js | 52 +++++++++++ 4 files changed, 307 insertions(+) create mode 100644 src/components/Sliding/index.js create mode 100644 src/components/Sliding/index.scss create mode 100644 src/components/Sliding/index.spec.js create mode 100644 src/stories/Sliding.stories.js diff --git a/src/components/Sliding/index.js b/src/components/Sliding/index.js new file mode 100644 index 00000000..3a7677cf --- /dev/null +++ b/src/components/Sliding/index.js @@ -0,0 +1,108 @@ +import { Component } from 'pet-dex-utilities'; +import { makeSwipable } from '../../utils/swiper'; +import './index.scss'; + +const events = [ + 'slide:add', + 'slide:next', + 'slide:previous', + 'slide:remove', + 'slides:clear', +]; + +const activeSlide = (slides, slide) => { + Array.from(slides).forEach((item) => { + item.classList.toggle('sliding__slide--active', item === slide); + }); +}; + +const html = ` +
+
+
+
`; + +export default function Sliding({ slides = [] }) { + Component.call(this, { html, events }); + + this.slideIndex = 0; + + slides.forEach((item) => this.add(item)); + + const $sliding = this.selected.get('sliding'); + + makeSwipable($sliding); + + this.swipeLeft = () => { + this.next(); + }; + + this.swipeRight = () => { + this.previous(); + }; + + this.listen('mount', () => { + activeSlide( + Array.from(this.selected.get('sliding-content').children), + this.selected.get('sliding-content').children[0], + ); + $sliding.addEventListener('swipe-left', this.swipeLeft()); + $sliding.addEventListener('swipe-right', this.swipeRight()); + }); + this.listen('unmount', () => { + $sliding.removeEventListener('swipe-left', this.swipeLeft()); + $sliding.removeEventListener('swipe-right', this.swipeRight()); + }); +} + +Sliding.prototype = Object.assign(Sliding.prototype, Component.prototype, { + add(slide) { + slide.classList.add('sliding__slide'); + this.selected.get('sliding-content').appendChild(slide); + + this.emit('slide:add', slide); + }, + + remove(slide) { + this.selected.get('sliding-content').removeChild(slide); + + this.emit('slide:remove', slide); + }, + + next() { + this.slideIndex += 1; + const slides = this.selected.get('sliding-content').children; + + if (this.slideIndex > slides.length - 1) this.slideIndex = 0; + + const slide = slides[this.slideIndex]; + const container = this.selected.get('sliding').clientWidth; + this.selected.get('sliding-content').style.transform = + `translateX(${-this.slideIndex * container}px)`; + activeSlide(slides, slide); + this.emit('slide:next', slide); + }, + + previous() { + this.slideIndex -= 1; + const slides = this.selected.get('sliding-content').children; + + if (this.slideIndex < 0) this.slideIndex = slides.length - 1; + + const slide = slides[this.slideIndex]; + const container = this.selected.get('sliding').clientWidth; + + this.selected.get('sliding-content').style.transform = + `translateX(${-this.slideIndex * container}px)`; + activeSlide(slides, slide); + this.emit('slide:previous', slide); + }, + + clear() { + Array.from(this.selected.get('sliding-content').children).forEach((slide) => + this.remove(slide), + ); + + this.emit('slides:clear'); + }, +}); diff --git a/src/components/Sliding/index.scss b/src/components/Sliding/index.scss new file mode 100644 index 00000000..5306fda2 --- /dev/null +++ b/src/components/Sliding/index.scss @@ -0,0 +1,20 @@ +.sliding { + width: 100%; + overflow: hidden; + + margin: auto; + + position: relative; + + &__content { + display: flex; + + transform: translateX(0); + + transition: transform 0.6s ease-in-out; + } + + &__slide { + flex: 0 0 100%; + } +} diff --git a/src/components/Sliding/index.spec.js b/src/components/Sliding/index.spec.js new file mode 100644 index 00000000..6b3fab41 --- /dev/null +++ b/src/components/Sliding/index.spec.js @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/vanilla'; +import Slinding from '.'; + +const $slide1 = document.createElement('div'); +$slide1.style.height = '200px'; +$slide1.style.backgroundColor = 'red'; +$slide1.textContent = 'slide 1'; + +const $slide2 = document.createElement('div'); +$slide2.style.height = '200px'; +$slide2.style.backgroundColor = 'pink'; +$slide2.textContent = 'slide 2'; + +const $slide3 = document.createElement('div'); +$slide3.style.height = '200px'; +$slide3.style.backgroundColor = 'green'; +$slide3.textContent = 'slide 3'; + +const $slide4 = document.createElement('div'); +$slide4.style.height = '200px'; +$slide4.style.backgroundColor = 'black'; +$slide4.textContent = 'slide 4'; + +const propsMock = { + slides: [$slide1, $slide2, $slide3], +}; + +const makeSut = (parameters) => render(new Slinding(parameters)); + +describe('Slide', () => { + describe('on mount', () => { + it('renders with items passed by props', async () => { + makeSut(propsMock); + const slide1 = await screen.findByText('slide 1'); + const slide2 = await screen.findByText('slide 2'); + const slide3 = await screen.findByText('slide 3'); + + expect(slide1).toBeInTheDocument(); + expect(slide2).toBeInTheDocument(); + expect(slide3).toBeInTheDocument(); + }); + }); + + describe('on unmount', () => { + it('clear items', () => { + const element = makeSut(propsMock); + + const callback = vi.fn(); + element.listen('unmount', callback); + + const slide1 = screen.queryByText('slide 1'); + const slide2 = screen.queryByText('slide 2'); + const slide3 = screen.queryByText('slide 3'); + + element.unmount(); + + expect(callback).toBeCalledWith(); + expect(slide1).not.toBeInTheDocument(); + expect(slide2).not.toBeInTheDocument(); + expect(slide3).not.toBeInTheDocument(); + }); + }); + + it('add item programmatically', async () => { + const sliding = makeSut(propsMock); + + const callback = vi.fn(); + sliding.listen('slide:add', callback); + sliding.add($slide4); + + const slide4 = await screen.findByText('slide 4'); + + expect(callback).toBeCalledWith($slide4); + expect(slide4).toBeInTheDocument(); + }); + + it('remove item programmatically', () => { + const sliding = makeSut(propsMock); + + const callback = vi.fn(); + sliding.listen('slide:remove', callback); + sliding.remove($slide2); + + const slide2 = screen.queryByText('slide 2'); + + expect(slide2).not.toBeInTheDocument(); + expect(callback).toHaveBeenCalledWith($slide2); + }); + + it('next item programmatically', () => { + const sliding = makeSut(propsMock); + + const callback = vi.fn(); + sliding.listen('slide:next', callback); + sliding.next(); + + expect(callback).toBeCalledWith($slide2); + }); + + it('previous item programmatically', () => { + const sliding = makeSut(propsMock); + + const callback = vi.fn(); + sliding.listen('slide:previous', callback); + sliding.previous(); + + expect(callback).toBeCalledWith($slide3); + }); + + it('clear items programmatically', () => { + const sliding = makeSut(propsMock); + + const callback = vi.fn(); + sliding.listen('slides:clear', callback); + sliding.clear(); + + const slide1 = screen.queryByText('slide 1'); + const slide2 = screen.queryByText('slide 2'); + const slide3 = screen.queryByText('slide 3'); + + expect(callback).toBeCalledWith(); + expect(slide1).not.toBeInTheDocument(); + expect(slide2).not.toBeInTheDocument(); + expect(slide3).not.toBeInTheDocument(); + }); +}); diff --git a/src/stories/Sliding.stories.js b/src/stories/Sliding.stories.js new file mode 100644 index 00000000..5cad90aa --- /dev/null +++ b/src/stories/Sliding.stories.js @@ -0,0 +1,52 @@ +import { initializeSwiper } from '../utils/swiper'; +import Sliding from '../components/Sliding'; +import Button from '../components/Button'; + +const $slide1 = document.createElement('div'); +$slide1.style.height = '200px'; +$slide1.style.backgroundColor = 'red'; + +const $slide2 = document.createElement('div'); +$slide2.style.height = '200px'; +$slide2.style.backgroundColor = 'pink'; + +const $slide3 = document.createElement('div'); +$slide3.style.height = '200px'; +$slide3.style.backgroundColor = 'green'; + +const button4 = new Button({ + text: '<', + isFullWidth: false, +}); + +const button5 = new Button({ + text: '>', + isFullWidth: false, +}); + +export default { + title: 'Components/Sliding', + render: (args) => { + const sliding = new Sliding(args); + const $container = document.createElement('div'); + initializeSwiper(); + window.sliding = sliding; + sliding.mount($container); + button4.mount($container); + button5.mount($container); + + button4.listen('click', () => sliding.previous()); + button5.listen('click', () => sliding.next()); + + return $container; + }, + argsTypes: { + slides: { control: 'object', defaultValue: [] }, + }, +}; + +export const Default = { + args: { + slides: [$slide1, $slide2, $slide3], + }, +};