Skip to content

Commit

Permalink
Feature/vertical image transition (#748)
Browse files Browse the repository at this point in the history
* Add vertical scrolling for image gallery

* Add veritical scroll prop and add base vertical scroll functionality

* Create svgs for top and bottom icons

* Create top and bottom nav components

* Render top and bottom arrows

* Add styling to top and bottom arrows

* Add initial logic for vetical swipe direction

* Add logic for vertical flicking

- Prevent touch screen devices from scrolling when transitioning slides on vertical scroll mode
- Ensure slides transition only for correct swipe directions

* Update example app to incorporate vertical sliding

* Remove unnecessary console log

* Resolve merge conflicts

* Change .js files to .jsx components

* Update import paths
  • Loading branch information
LefBoutham committed Jan 15, 2024
1 parent e456451 commit 5067a49
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@
}
}
]
}
}
14 changes: 14 additions & 0 deletions example/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class App extends React.Component {
showPlayButton: true,
showGalleryPlayButton: true,
showNav: true,
slideVertically: false,
isRTL: false,
slideDuration: 450,
slideInterval: 2000,
Expand Down Expand Up @@ -201,6 +202,7 @@ class App extends React.Component {
slideOnThumbnailOver={this.state.slideOnThumbnailOver}
additionalClass="app-image-gallery"
useWindowKeyDown={this.state.useWindowKeyDown}
slideVertically={this.state.slideVertically}
/>

<div className="app-sandbox">
Expand Down Expand Up @@ -333,6 +335,18 @@ class App extends React.Component {
/>
<label htmlFor="show_index">show index</label>
</li>
<li>
<input
id="slide_vertically"
type="checkbox"
onChange={this._handleCheckboxChange.bind(
this,
"slideVertically"
)}
checked={this.state.slideVertically}
/>
<label htmlFor="slide_vertically">slide vertically</label>
</li>
<li>
<input
id="is_rtl"
Expand Down
109 changes: 86 additions & 23 deletions src/components/ImageGallery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import LeftNav from "src/components/controls/LeftNav";
import RightNav from "src/components/controls/RightNav";
import PlayPause from "src/components/controls/PlayPause";
import SwipeWrapper from "src/components/SwipeWrapper";
import TopNav from "src/components/controls/TopNav";
import BottomNav from "src/components/controls/BottomNav";

const screenChangeEvents = [
"fullscreenchange",
Expand Down Expand Up @@ -473,37 +475,42 @@ class ImageGallery extends React.Component {
}

getSlideStyle(index) {
const { currentIndex, currentSlideOffset, slideStyle } = this.state;
const { infinite, items, useTranslate3D, isRTL } = this.props;
const { currentIndex, currentSlideOffset, slideStyle } =
this.state;
const { infinite, items, useTranslate3D, isRTL, slideVertically } = this.props;
const baseTranslateX = -100 * currentIndex;
const totalSlides = items.length - 1;

// calculates where the other slides belong based on currentIndex
// if it is RTL the base line should be reversed
let translateX =
let translateValue =
(baseTranslateX + index * 100) * (isRTL ? -1 : 1) + currentSlideOffset;

if (infinite && items.length > 2) {
if (currentIndex === 0 && index === totalSlides) {
// make the last slide the slide before the first
// if it is RTL the base line should be reversed
translateX = -100 * (isRTL ? -1 : 1) + currentSlideOffset;
translateValue = -100 * (isRTL ? -1 : 1) + currentSlideOffset;
} else if (currentIndex === totalSlides && index === 0) {
// make the first slide the slide after the last
// if it is RTL the base line should be reversed
translateX = 100 * (isRTL ? -1 : 1) + currentSlideOffset;
translateValue = 100 * (isRTL ? -1 : 1) + currentSlideOffset;
}
}

// Special case when there are only 2 items with infinite on
if (infinite && items.length === 2) {
translateX = this.getTranslateXForTwoSlide(index);
translateValue = this.getTranslateXForTwoSlide(index);
}

let translate = `translate(${translateX}%, 0)`;
let translate = slideVertically
? `translate(0, ${translateValue}%)`
: `translate(${translateValue}%, 0)`;

if (useTranslate3D) {
translate = `translate3d(${translateX}%, 0, 0)`;
translate = slideVertically
? `translate3d(0, ${translateValue}%, 0)`
: `translate3d(${translateValue}%, 0, 0)`;
}

// don't show some slides while transitioning to avoid background transitions
Expand Down Expand Up @@ -781,16 +788,23 @@ class ImageGallery extends React.Component {
}

handleSwiping({ event, absX, dir }) {
const { disableSwipe, stopPropagation } = this.props;
const { galleryWidth, isTransitioning, swipingUpDown, swipingLeftRight } =
this.state;
const { disableSwipe, stopPropagation, swipingTransitionDuration } =
this.props;
const {
galleryWidth,
isTransitioning,
swipingUpDown,
swipingLeftRight,
} = this.state;

const { slideVertically } = this.props;

// if the initial swiping is up/down prevent moving the slides until swipe ends
if ((dir === UP || dir === DOWN || swipingUpDown) && !swipingLeftRight) {
if (!swipingUpDown) {
this.setState({ swipingUpDown: true });
}
return;
if (!slideVertically) return;
}

if ((dir === LEFT || dir === RIGHT) && !swipingLeftRight) {
Expand All @@ -799,15 +813,28 @@ class ImageGallery extends React.Component {

if (disableSwipe) return;

const { swipingTransitionDuration } = this.props;
if (stopPropagation) {
event.preventDefault();
}

if (!isTransitioning) {
const side = dir === RIGHT ? 1 : -1;
const isSwipeLeftOrRight = dir === LEFT || dir === RIGHT;
const isSwipeTopOrDown = dir === UP || dir === DOWN;

if (isSwipeLeftOrRight && slideVertically) return;
if (isSwipeTopOrDown && !slideVertically) return;

const sides = {
[LEFT]: -1,
[RIGHT]: 1,
[UP]: -1,
[DOWN]: 1,
};

const side = sides[dir];

let currentSlideOffset = (absX / galleryWidth) * 100;

if (Math.abs(currentSlideOffset) >= 100) {
currentSlideOffset = 100;
}
Expand Down Expand Up @@ -953,6 +980,7 @@ class ImageGallery extends React.Component {

handleOnSwiped({ event, dir, velocity }) {
const { disableSwipe, stopPropagation, flickThreshold } = this.props;
const { slideVertically } = this.props;

if (disableSwipe) return;

Expand All @@ -961,17 +989,24 @@ class ImageGallery extends React.Component {
this.resetSwipingDirection();

// if it is RTL the direction is reversed
const swipeDirection = (dir === LEFT ? 1 : -1) * (isRTL ? -1 : 1);
let swipeDirection = (dir === LEFT ? 1 : -1) * (isRTL ? -1 : 1);
if (slideVertically) swipeDirection = dir === UP ? 1 : -1;

const isSwipeUpOrDown = dir === UP || dir === DOWN;
const isSwipeLeftOrRight = dir === LEFT || dir === RIGHT;
const isLeftRightFlick = velocity > flickThreshold && !isSwipeUpOrDown;
this.handleOnSwipedTo(swipeDirection, isLeftRightFlick);
const isTopDownFlick = velocity > flickThreshold && !isSwipeLeftOrRight;

const isFlick = slideVertically ? isTopDownFlick : isLeftRightFlick;

this.handleOnSwipedTo(swipeDirection, isFlick);
}

handleOnSwipedTo(swipeDirection, isLeftRightFlick) {
handleOnSwipedTo(swipeDirection, isFlick) {
const { currentIndex, isTransitioning } = this.state;
let slideTo = currentIndex;

if ((this.sufficientSwipe() || isLeftRightFlick) && !isTransitioning) {
if ((this.sufficientSwipe() || isFlick) && !isTransitioning) {
// slideto the next/prev slide
slideTo += swipeDirection;
}
Expand Down Expand Up @@ -1437,8 +1472,14 @@ class ImageGallery extends React.Component {
}

render() {
const { currentIndex, isFullscreen, modalFullscreen, isPlaying } =
this.state;
const {
currentIndex,
isFullscreen,
modalFullscreen,
isPlaying,
} = this.state;

const { slideVertically } = this.props;

const {
additionalClass,
Expand All @@ -1451,6 +1492,8 @@ class ImageGallery extends React.Component {
renderCustomControls,
renderLeftNav,
renderRightNav,
renderTopNav,
renderBottomNav,
showBullets,
showFullscreenButton,
showIndex,
Expand All @@ -1475,8 +1518,12 @@ class ImageGallery extends React.Component {
<React.Fragment>
{showNav && (
<React.Fragment>
{renderLeftNav(this.slideLeft, !this.canSlideLeft())}
{renderRightNav(this.slideRight, !this.canSlideRight())}
{slideVertically
? renderTopNav(this.slideLeft, !this.canSlideLeft())
: renderLeftNav(this.slideLeft, !this.canSlideLeft())}
{slideVertically
? renderBottomNav(this.slideRight, !this.canSlideRight())
: renderRightNav(this.slideRight, !this.canSlideRight())}
</React.Fragment>
)}
<SwipeWrapper
Expand All @@ -1485,7 +1532,13 @@ class ImageGallery extends React.Component {
onSwiping={this.handleSwiping}
onSwiped={this.handleOnSwiped}
>
<div className="image-gallery-slides">{slides}</div>
<div
className="image-gallery-slides"
// disable touch-action for touch screen devices when vertical sliding is active
style={{ touchAction: slideVertically ? "none" : "unset" }}
>
{slides}
</div>
</SwipeWrapper>
</React.Fragment>
) : (
Expand Down Expand Up @@ -1649,6 +1702,8 @@ ImageGallery.propTypes = {
renderCustomControls: func,
renderLeftNav: func,
renderRightNav: func,
renderTopNav: func,
renderBottomNav: func,
renderPlayPauseButton: func,
renderFullscreenButton: func,
renderItem: func,
Expand All @@ -1658,6 +1713,7 @@ ImageGallery.propTypes = {
useTranslate3D: bool,
isRTL: bool,
useWindowKeyDown: bool,
slideVertically: bool,
};

ImageGallery.defaultProps = {
Expand Down Expand Up @@ -1709,12 +1765,19 @@ ImageGallery.defaultProps = {
slideInterval: 3000,
slideOnThumbnailOver: false,
swipeThreshold: 30,
slideVertically: false,
renderLeftNav: (onClick, disabled) => (
<LeftNav onClick={onClick} disabled={disabled} />
),
renderRightNav: (onClick, disabled) => (
<RightNav onClick={onClick} disabled={disabled} />
),
renderTopNav: (onClick, disabled) => (
<TopNav onClick={onClick} disabled={disabled} />
),
renderBottomNav: (onClick, disabled) => (
<BottomNav onClick={onClick} disabled={disabled} />
),
renderPlayPauseButton: (onClick, isPlaying) => (
<PlayPause onClick={onClick} isPlaying={isPlaying} />
),
Expand Down
16 changes: 14 additions & 2 deletions src/components/SVG.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { number, oneOf, string } from "prop-types";

const left = <polyline points="15 18 9 12 15 6" />;
const right = <polyline points="9 18 15 12 9 6" />;
const top = <polyline points="6 15 12 9 18 15" />;
const bottom = <polyline points="6 9 12 15 18 9" />;
const maximize = (
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
);
Expand All @@ -20,6 +22,8 @@ const pause = (
const iconMapper = {
left,
right,
top,
bottom,
maximize,
minimize,
play,
Expand Down Expand Up @@ -52,8 +56,16 @@ const SVG = (props) => {
SVG.propTypes = {
strokeWidth: number,
viewBox: string,
icon: oneOf(["left", "right", "maximize", "minimize", "play", "pause"])
.isRequired,
icon: oneOf([
"left",
"right",
"top",
"bottom",
"maximize",
"minimize",
"play",
"pause",
]).isRequired,
};

export default SVG;
26 changes: 26 additions & 0 deletions src/components/controls/BottomNav.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { bool, func } from "prop-types";
import SVG from "src/components/SVG";

const BottomNav = React.memo(({ disabled, onClick }) => {
return (
<button
type="button"
className="image-gallery-icon image-gallery-bottom-nav"
disabled={disabled}
onClick={onClick}
aria-label="Next Slide"
>
<SVG icon="bottom" viewBox="6 0 12 24" />
</button>
);
});

BottomNav.displayName = "BottomNav";

BottomNav.propTypes = {
disabled: bool.isRequired,
onClick: func.isRequired,
};

export default BottomNav;
26 changes: 26 additions & 0 deletions src/components/controls/TopNav.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { bool, func } from "prop-types";
import SVG from "src/components/SVG";

const TopNav = React.memo(({ disabled, onClick }) => {
return (
<button
type="button"
className="image-gallery-icon image-gallery-top-nav"
disabled={disabled}
onClick={onClick}
aria-label="Previous Slide"
>
<SVG icon="top" viewBox="6 0 12 24" />
</button>
);
});

TopNav.displayName = "TopNav";

TopNav.propTypes = {
disabled: bool.isRequired,
onClick: func.isRequired,
};

export default TopNav;
Loading

0 comments on commit 5067a49

Please sign in to comment.