From 844e0a10b21cd39320e3b11811c12721978994bc Mon Sep 17 00:00:00 2001 From: ZIA <1025013204@qq.com> Date: Tue, 21 May 2024 16:35:32 +0800 Subject: [PATCH] feat(carousel): add vertical mode for carousel (#137) * feat(carousel): add vertical mode for carousel * fix: change for code reviews * fix: change for code reviews * chore: new version * docs(carousel): add html source code for vertical --- .changeset/hot-candles-fly.md | 6 + docs/example/Carousel/demos/vertical.tsx | 35 ++++++ docs/example/Carousel/index.md | 3 + packages/banana/src/carousel/index.styles.ts | 60 ++++++++-- packages/banana/src/carousel/index.ts | 118 +++++++++++++------ public/Carousel/vertical.html | 20 ++++ 6 files changed, 201 insertions(+), 41 deletions(-) create mode 100644 .changeset/hot-candles-fly.md create mode 100644 docs/example/Carousel/demos/vertical.tsx create mode 100644 public/Carousel/vertical.html diff --git a/.changeset/hot-candles-fly.md b/.changeset/hot-candles-fly.md new file mode 100644 index 00000000..ece61946 --- /dev/null +++ b/.changeset/hot-candles-fly.md @@ -0,0 +1,6 @@ +--- +'@banana-ui/banana': patch +'@banana-ui/react': patch +--- + +add vertical mode for carousel diff --git a/docs/example/Carousel/demos/vertical.tsx b/docs/example/Carousel/demos/vertical.tsx new file mode 100644 index 00000000..1a37de3e --- /dev/null +++ b/docs/example/Carousel/demos/vertical.tsx @@ -0,0 +1,35 @@ +/** + * title: 垂直展示 + * description: 当需要垂直展示时,需要手动指定容器高度(设置对应css变量`--banana-carousel-vertical-height`) + */ + +import { Carousel } from '@banana-ui/react'; + +export default function Vertical() { + const style = ` + .demo-slide--vertical { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(to bottom right, #5193e9, #0e61cd); + color: #fff; + font-size: 48px; + } + .container { + --banana-carousel-vertical-height: 300px; + } + `; + + return ( + <> + + +
1
+
2
+
3
+
4
+
5
+
+ + ); +} diff --git a/docs/example/Carousel/index.md b/docs/example/Carousel/index.md index 9d8b79ff..ba2e2eb2 100644 --- a/docs/example/Carousel/index.md +++ b/docs/example/Carousel/index.md @@ -26,6 +26,7 @@ group: 组件 + ## 属性 - Attributes & Properties @@ -42,6 +43,7 @@ group: 组件 | disableFill
(disable-fill) | 默认情况下,当你传入的轮播图个数小于 slidesPerView 时,会自动填充,传入此属性可以取消这个行为 | `boolean` | false | | autoHeight | 是否自动调整高度 | `boolean` | false | | indicator | 是否显示指示器(默认为小圆点) | `boolean` | false | +| vertical | 是否垂直展示 | `boolean` | false | ## 方法 - Methods @@ -90,3 +92,4 @@ group: 组件 | --banana-carousel-indicator-gap | 指示器之间的间隔 | `--banana-carousel-indicator-size` | | --banana-carousel-indicator-size | 指示器的大小 | 8px | | --banana-carousel-indicator-color | 指示器的颜色(非激活状态的指示器`opacity`为 0.5) | rgba(46, 50, 56, 1) | +| --banana-carousel-vertical-height | 垂直展示时,需要指定容器的高度 | 100% | diff --git a/packages/banana/src/carousel/index.styles.ts b/packages/banana/src/carousel/index.styles.ts index 629c46bc..b21d03b0 100644 --- a/packages/banana/src/carousel/index.styles.ts +++ b/packages/banana/src/carousel/index.styles.ts @@ -25,6 +25,10 @@ export default [ transition: height var(--banana-carousel-transition-duration, ${unsafeCSS(Var.TransitionNormal)}); } + .external-wrapper--vertical { + height: var(--banana-carousel-vertical-height, 100%); + } + .slides-wrapper { display: flex; transition: transform var(--banana-carousel-transition-duration, ${unsafeCSS(Var.TransitionNormal)}); @@ -32,12 +36,16 @@ export default [ /* DO NOT PUT IT IN THE DOCUMENTATION */ gap: calc(var(--banana-carousel-gap, 0) * 1px); } + .slides-wrapper--vertical { + flex-direction: column; + height: 100%; + } .slides-wrapper.no-transition { transition-duration: 0ms; } - .slides-wrapper ::slotted(*) { + .slides-wrapper--normal ::slotted(*) { /* This variable should not be used directly, use the slidesPerView property instead */ /* DO NOT PUT IT IN THE DOCUMENTATION */ width: calc( @@ -49,10 +57,20 @@ export default [ flex-shrink: 0; } + .slides-wrapper--vertical ::slotted(*) { + /* This variable should not be used directly, use the slidesPerView property instead */ + /* DO NOT PUT IT IN THE DOCUMENTATION */ + height: calc( + (100% - (var(--banana-carousel-slidesPerView) - 1) * var(--banana-carousel-gap) * 1px) / + var(--banana-carousel-slidesPerView) + ); + width: 100%; + flex-grow: 0; + flex-shrink: 0; + } + .navigation-buttons { position: absolute; - top: 50%; - transform: translateY(-50%); margin: 0; padding: 0; border: none; @@ -63,6 +81,16 @@ export default [ z-index: 1; } + .navigation-button--normal { + top: 50%; + transform: translateY(-50%); + } + + .navigation-button--vertical { + left: 50%; + transform: translateX(-50%) rotate(90deg); + } + .navigation-button--disabled { opacity: 0.4; cursor: default; @@ -72,13 +100,19 @@ export default [ background-color: rgba(${unsafeCSS(Colors.Gray2)}, 0.5); } - .navigation-button--previous { + .navigation-button--previous__normal { left: 10px; } + .navigation-button--previous__vertical { + top: 10px; + } - .navigation-button--next { + .navigation-button--next__normal { right: 10px; } + .navigation-button--next__vertical { + bottom: 10px; + } .default-prev-icon, .default-next-icon { @@ -90,9 +124,6 @@ export default [ .indicators { position: absolute; - bottom: 16px; - left: 50%; - transform: translateX(-50%); margin: 0; padding: 0; list-style: none; @@ -102,6 +133,19 @@ export default [ z-index: 1; } + .indicators--normal { + bottom: 16px; + left: 50%; + transform: translateX(-50%); + } + + .indicators--vertical { + right: 16px; + bottom: 50%; + transform: translateY(50%); + flex-direction: column; + } + .indicator { width: var(--banana-carousel-indicator-size, 8px); height: var(--banana-carousel-indicator-size, 8px); diff --git a/packages/banana/src/carousel/index.ts b/packages/banana/src/carousel/index.ts index 685c3221..81bbcd82 100644 --- a/packages/banana/src/carousel/index.ts +++ b/packages/banana/src/carousel/index.ts @@ -122,6 +122,9 @@ export default class BCarousel extends LitElement { @property({ type: Boolean, reflect: true }) indicator = false; + @property({ type: Boolean }) + vertical = false; + @query('.external-wrapper') _externalWrapper: HTMLDivElement | undefined; @@ -146,12 +149,16 @@ export default class BCarousel extends LitElement { @state() autoplayTimer: ReturnType | undefined; - private get _externalWrapperWidth() { - return this._externalWrapper?.getBoundingClientRect().width || 0; + private get _externalWrapperSize() { + return ( + (this.vertical + ? this._externalWrapper?.getBoundingClientRect().height + : this._externalWrapper?.getBoundingClientRect().width) ?? 0 + ); } - private get _slideWidth() { - return (this._externalWrapperWidth - (this._slidesPerView - 1) * this.gap) / this._slidesPerView; + private get _slideUnitSize() { + return (this._externalWrapperSize - (this._slidesPerView - 1) * this.gap) / this._slidesPerView; } private get MIN() { @@ -162,8 +169,12 @@ export default class BCarousel extends LitElement { return this._slides.length - 1; } - private get totalWidth() { - return this._slideWidth * this._slides.length + this._slides.length * this.gap; + private get totalSlidesSizeWithGap() { + return this._slideUnitSize * this._slides.length + this._slides.length * this.gap; + } + + private get coordinateDirection() { + return this.vertical ? 'y' : 'x'; } // Record how many cycles have been made if `loop` is true. @@ -285,10 +296,12 @@ export default class BCarousel extends LitElement { private _repositioningSlides() { if (this._loop) { - const translateValue = this._loopCount * this.totalWidth; + const translateValue = this._loopCount * this.totalSlidesSizeWithGap; for (const slide of this._slides) { - slide.style.transform = `translate3d(${translateValue}px, 0, 0)`; + slide.style.transform = this.vertical + ? `translate3d(0, ${translateValue}px, 0)` + : `translate3d(${translateValue}px, 0, 0)`; } } } @@ -303,9 +316,11 @@ export default class BCarousel extends LitElement { } } - const slideWidthWithGap = this._slideWidth + this.gap; - const translateValue = this._loopCount * this.totalWidth - slideWidthWithGap * this._slidesPerView || 0; - const _translateValue = (this._loopCount - 1) * this.totalWidth - slideWidthWithGap * this._slidesPerView || 0; + const slideWidthWithGap = this._slideUnitSize + this.gap; + const translateValue = + this._loopCount * this.totalSlidesSizeWithGap - slideWidthWithGap * this._slidesPerView || 0; + const _translateValue = + (this._loopCount - 1) * this.totalSlidesSizeWithGap - slideWidthWithGap * this._slidesPerView || 0; // Those copys will append to the beginning of slides. const CopysAtTheBeginning = []; @@ -325,13 +340,17 @@ export default class BCarousel extends LitElement { for (let i = 0; i < this._slidesPerView; i++) { const copyAtTheBeginning = CopysAtTheBeginning[i]; copyAtTheBeginning.setAttribute('data-clone', String(this._slides.length - this._slidesPerView + i)); - copyAtTheBeginning.style.transform = `translate3d(${_translateValue}px, 0, 0)`; + copyAtTheBeginning.style.transform = this.vertical + ? `translate3d(0, ${_translateValue}px, 0)` + : `translate3d(${_translateValue}px, 0, 0)`; this.append(copyAtTheBeginning); } for (let i = 0; i < this._slidesPerView; i++) { const copyAtTheEnd = CopysAtTheEnd[i]; copyAtTheEnd.setAttribute('data-clone', String(i)); - copyAtTheEnd.style.transform = `translate3d(${translateValue}px, 0, 0)`; + copyAtTheEnd.style.transform = this.vertical + ? `translate3d(0, ${translateValue}px, 0)` + : `translate3d(${translateValue}px, 0, 0)`; this.append(copyAtTheEnd); } } @@ -362,8 +381,11 @@ export default class BCarousel extends LitElement { // When start dragging, that _pointerStartX obviously won't be undefined. // Dragging can only occur after DragStart, and the _onDragStart function will set the _pointerStartX. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._dragDistance = this._pointerCurrentX - this._pointerStartX!; + this._dragDistance = this.vertical + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._pointerCurrentY - this._pointerStartY! + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._pointerCurrentX - this._pointerStartX!; } private _onDragEnd(e: Event) { @@ -380,28 +402,33 @@ export default class BCarousel extends LitElement { this._trackingCoordinates = []; - const diffX = lastTrackingCoordinate.x - firstTrackingCoordinate.x; - // const diffY = lastTrackingCoordinate.y - firstTrackingCoordinate.y; + const diffMoveDistance = + lastTrackingCoordinate[this.coordinateDirection] - firstTrackingCoordinate[this.coordinateDirection]; const diffTime = lastTrackingCoordinate.time - firstTrackingCoordinate.time; this._dragDistance = 0; - const speedX = Math.abs(diffX / diffTime); - if (speedX >= this._minSpeedToMoveX) { - if (diffX < 0) { + const speed = Math.abs(diffMoveDistance / diffTime); + + const isOverMinSpeed = this.vertical ? speed >= this._minSpeedToMoveY : speed >= this._minSpeedToMoveX; + if (isOverMinSpeed) { + if (diffMoveDistance < 0) { this.next(); } else { this.prev(); } } else { // Move if speed is not enough but dragging more than half. - const wrapperX = this._externalWrapper?.getBoundingClientRect().x || 0; - const distanceFromSlidesToWrapper = this._slides.map((item) => item.getBoundingClientRect().x - wrapperX); + const wrapperCoordinateOfDirection = + this._externalWrapper?.getBoundingClientRect()[this.coordinateDirection] || 0; + const distanceFromSlidesToWrapper = this._slides.map( + (item) => item.getBoundingClientRect()[this.coordinateDirection] - wrapperCoordinateOfDirection, + ); const distanceOfCurrentSlidesToWrapper = distanceFromSlidesToWrapper[this.currentIndex]; - if (distanceOfCurrentSlidesToWrapper < 0 && -distanceOfCurrentSlidesToWrapper > this._slideWidth / 2) { + if (distanceOfCurrentSlidesToWrapper < 0 && -distanceOfCurrentSlidesToWrapper > this._slideUnitSize / 2) { this.next(); - } else if (distanceOfCurrentSlidesToWrapper > 0 && distanceOfCurrentSlidesToWrapper > this._slideWidth / 2) { + } else if (distanceOfCurrentSlidesToWrapper > 0 && distanceOfCurrentSlidesToWrapper > this._slideUnitSize / 2) { this.prev(); } } @@ -435,13 +462,17 @@ export default class BCarousel extends LitElement { } private _externalWrapperTranslate() { - const wholeWidth = this._slideWidth + this.gap; + const wholeDistance = this._slideUnitSize + this.gap; if (this._loop) { - const loopShift = -(this.totalWidth * this._loopCount); - return -this.currentIndex * wholeWidth + this._dragDistance + loopShift; + const loopShift = -(this.totalSlidesSizeWithGap * this._loopCount); + return this.vertical + ? [0, -this.currentIndex * wholeDistance + this._dragDistance + loopShift] + : [-this.currentIndex * wholeDistance + this._dragDistance + loopShift, 0]; } else { - return -this.currentIndex * wholeWidth + this._dragDistance; + return this.vertical + ? [0, -this.currentIndex * wholeDistance + this._dragDistance] + : [-this.currentIndex * wholeDistance + this._dragDistance, 0]; } } @@ -467,6 +498,8 @@ export default class BCarousel extends LitElement { const previousNavigationDisabled = this._computePrev(this.currentIndex) === this.currentIndex; const nextNavigationDisabled = this._computeNext(this.currentIndex) === this.currentIndex; + const [translateX, translateY] = this._externalWrapperTranslate(); + return html`
@@ -486,16 +522,26 @@ export default class BCarousel extends LitElement { @touchstart="${this._eventHandler}" class=${classMap({ 'slides-wrapper': true, + 'slides-wrapper--normal': !this.vertical, + 'slides-wrapper--vertical': this.vertical, 'no-transition': this._isDragging, })} - style="transform: translate3d(${this._externalWrapperTranslate()}px, 0px, 0px); --banana-carousel-slidesPerView: ${this + style="transform: translate3d(${translateX}px, ${translateY}px, 0px); --banana-carousel-slidesPerView: ${this ._slidesPerView}; --banana-carousel-gap: ${this.gap}" >
-