Skip to content

Commit

Permalink
feat(home): add pagination dots to feature shelf
Browse files Browse the repository at this point in the history
  • Loading branch information
RCVZ committed Jul 20, 2021
1 parent 5717d6c commit 0317426
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 48 deletions.
17 changes: 17 additions & 0 deletions src/components/Shelf/Shelf.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@
transform: scale(1.2);
}
}

.dot {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 5px;
background-color: rgba(254, 254, 254, 0.2);
border-radius: 50%;
cursor: pointer;
transition: all 200ms ease;
&.active,
&:hover {
background-color: var(--primary-color);
transform: scale(1.1);
}
}

.error {
color: var(--card-color);
font-family: var(--body-alt-font-family);
Expand Down
13 changes: 13 additions & 0 deletions src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ const Shelf: React.FC<ShelfProps> = ({
[didSlideBefore, t],
);

const renderPaginationDots = (onClick: () => void, index: number, pageIndex: number) => (
<span
key={pageIndex}
className={classNames(styles.dot, { [styles.active]: index === pageIndex })}
onClick={onClick}
role="button"
aria-pressed={index === pageIndex}
aria-label={t('show_page', { number: pageIndex + 1 })}
/>
);

const handleSlide = (doSlide: () => void): void => {
setDidSlideBefore(true);
doSlide();
Expand All @@ -131,10 +142,12 @@ const Shelf: React.FC<ShelfProps> = ({
wrapWithEmptyTiles={featured && playlist.playlist.length === 1}
cycleMode={'restart'}
showControls={!matchMedia('(hover: none)').matches && !loading}
showDots={featured}
transitionTime={'0.3s'}
spacing={8}
renderLeftControl={renderLeftControl}
renderRightControl={renderRightControl}
renderPaginationDots={renderPaginationDots}
renderTile={renderTile}
/>
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/components/TileDock/TileDock.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@
background: rgba(255, 255, 255, 0.12);
border-radius: 4px;
}

.dots {
position: relative;
display: flex;
justify-content: center;
width: 100%;
margin-top: 12px;
}
139 changes: 94 additions & 45 deletions src/components/TileDock/TileDock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState, useEffect } from 'react';

import styles from './TileDock.module.scss';

Expand All @@ -15,12 +15,14 @@ export type TileDockProps<T> = {
tileHeight?: number;
minimalTouchMovement?: number;
showControls?: boolean;
showDots?: boolean;
animated?: boolean;
wrapWithEmptyTiles?: boolean;
transitionTime?: string;
renderTile: (item: T, isInView: boolean) => JSX.Element;
renderLeftControl?: (handleClick: () => void) => JSX.Element;
renderRightControl?: (handleClick: () => void) => JSX.Element;
renderPaginationDots?: (handleClick: () => void, index: number, pageIndex: number) => JSX.Element;
};

type Tile<T> = {
Expand Down Expand Up @@ -65,9 +67,11 @@ const TileDock = <T extends unknown>({
animated = !window.matchMedia('(prefers-reduced-motion)').matches,
transitionTime = '0.6s',
wrapWithEmptyTiles = false,
showDots = false,
renderTile,
renderLeftControl,
renderRightControl,
renderPaginationDots,
}: TileDockProps<T>) => {
const [index, setIndex] = useState<number>(0);
const [slideToIndex, setSlideToIndex] = useState<number>(0);
Expand All @@ -77,39 +81,48 @@ const TileDock = <T extends unknown>({
const tileWidth: number = 100 / tilesToShow;
const isMultiPage: boolean = items?.length > tilesToShow;
const transformWithOffset: number = isMultiPage ? 100 - tileWidth * (tilesToShow + 1) + transform : wrapWithEmptyTiles ? -100 : 0;

const [slideQueue, updateSlideQueue] = useState<string[]>([]);
const [slideAgain, setSlideAgain] = useState(false);
const pages = items.length / tilesToShow;
const tileList: Tile<T>[] = useMemo(() => {
return sliceItems<T>(items, isMultiPage, index, tilesToShow, cycleMode);
}, [items, isMultiPage, index, tilesToShow, cycleMode]);

const transitionBasis: string = isMultiPage && animated ? `transform ${transitionTime} ease` : '';
const transitionBasis: string = isMultiPage && animated ? `transform ${slideQueue.length ? '200ms' : transitionTime} ease` : '';

const needControls: boolean = showControls && isMultiPage;
const showLeftControl: boolean = needControls && !(cycleMode === 'stop' && index === 0);
const showRightControl: boolean = needControls && !(cycleMode === 'stop' && index === items.length - tilesToShow);

const slide = useCallback(
(direction: Direction): void => {
(direction: Direction, slides: number = 0): void => {
const directionFactor = direction === 'right' ? 1 : -1;
let nextIndex: number = index + tilesToShow * directionFactor;

if (nextIndex < 0) {
if (cycleMode === 'stop') nextIndex = 0;
if (cycleMode === 'restart') nextIndex = index === 0 ? 0 - tilesToShow : 0;
}

if (nextIndex > items.length - tilesToShow) {
if (cycleMode === 'stop') nextIndex = items.length - tilesToShow;
if (cycleMode === 'restart') nextIndex = index >= items.length - tilesToShow ? items.length : items.length - tilesToShow;
}
if (index !== slideToIndex) {
updateSlideQueue((slideQueue) => [...slideQueue, direction]);
return;
}

const steps: number = Math.abs(index - nextIndex);
const movement: number = steps * tileWidth * (0 - directionFactor);

setSlideToIndex(nextIndex);
setTransform(-100 + movement);

if (slides) updateSlideQueue(Array.from({ length: slides - 1 }, () => direction));
if (!animated) setDoAnimationReset(true);
},
[animated, cycleMode, index, items.length, tileWidth, tilesToShow],
[animated, cycleMode, index, items.length, tileWidth, tilesToShow, slideToIndex],
);

const handleTouchStart = useCallback(
Expand Down Expand Up @@ -179,16 +192,33 @@ const TileDock = <T extends unknown>({
}

setIndex(resetIndex);

if (frameRef.current) frameRef.current.style.transition = 'none';
setTransform(-100);

setTimeout(() => {
if (frameRef.current) frameRef.current.style.transition = transitionBasis;
}, 0);
setDoAnimationReset(false);

if (slideQueue) {
setSlideAgain(true);
}
};

if (doAnimationReset) resetAnimation();
}, [doAnimationReset, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis]);
}, [doAnimationReset, index, items.length, slideToIndex, tileWidth, tilesToShow, transitionBasis, slide, slideQueue]);

useEffect(() => {
if (slideAgain && slideQueue.length) {
const slideQueuePointer = [...slideQueue];
const direction = slideQueuePointer.shift();

updateSlideQueue(slideQueuePointer);
slide(direction as Direction);
setSlideAgain(false);
}
}, [slideAgain, slide, slideQueue]);

const handleTransitionEnd = (event: React.TransitionEvent<HTMLUListElement>) => {
if (event.target === frameRef.current) {
Expand All @@ -207,53 +237,72 @@ const TileDock = <T extends unknown>({

const slideOffset = index - slideToIndex;

const paginationDots = () => {
if (showDots && isMultiPage && !!renderPaginationDots) {
const length = pages;
const dotsRow = (
<div className={styles.dots}>
{Array.from({ length }, (_, pageIndex) => {
const direction = index < pageIndex + 1 ? 'right' : 'left';
const slides = Math.abs(pageIndex - index);
return renderPaginationDots(() => index !== pageIndex && slide(direction, slides), index, pageIndex);
})}
</div>
);
return dotsRow;
}
};

return (
<div className={styles.tileDock}>
{showLeftControl && !!renderLeftControl && <div className={styles.leftControl}>{renderLeftControl(() => slide('left'))}</div>}
<ul ref={frameRef} style={ulStyle} onTouchStart={handleTouchStart} onTransitionEnd={handleTransitionEnd}>
{wrapWithEmptyTiles ? (
<li
className={styles.emptyTile}
style={{
width: `${tileWidth}%`,
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
}}
/>
) : null}
{tileList.map((tile: Tile<T>, listIndex) => {
const isInView = !isMultiPage || (listIndex > tilesToShow - slideOffset && listIndex < tilesToShow * 2 + 1 - slideOffset);

return (
<div>
<div className={styles.tileDock}>
{showLeftControl && !!renderLeftControl && <div className={styles.leftControl}>{renderLeftControl(() => slide('left'))}</div>}
<ul ref={frameRef} style={ulStyle} onTouchStart={handleTouchStart} onTransitionEnd={handleTransitionEnd}>
{wrapWithEmptyTiles ? (
<li
className={styles.emptyTile}
style={{
width: `${tileWidth}%`,
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
}}
/>
) : null}
{tileList.map((tile: Tile<T>, listIndex) => {
const isInView = !isMultiPage || (listIndex > tilesToShow - slideOffset && listIndex < tilesToShow * 2 + 1 - slideOffset);

return (
<li
key={tile.key}
className={classNames({ [styles.notInView]: !isInView })}
style={{
width: `${tileWidth}%`,
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
transition: !isInView ? 'opacity .2s ease-in 0s' : '',
}}
>
{renderTile(tile.item, isInView)}
</li>
);
})}
{wrapWithEmptyTiles ? (
<li
key={tile.key}
className={classNames({ [styles.notInView]: !isInView })}
className={styles.emptyTile}
style={{
width: `${tileWidth}%`,
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
transition: !isInView ? 'opacity .2s ease-in 0s' : '',
}}
>
{renderTile(tile.item, isInView)}
</li>
);
})}
{wrapWithEmptyTiles ? (
<li
className={styles.emptyTile}
style={{
width: `${tileWidth}%`,
paddingLeft: spacing / 2,
paddingRight: spacing / 2,
boxSizing: 'border-box',
}}
/>
) : null}
</ul>
{showRightControl && !!renderRightControl && <div className={styles.rightControl}>{renderRightControl(() => slide('right'))}</div>}
/>
) : null}
</ul>
{showRightControl && !!renderRightControl && <div className={styles.rightControl}>{renderRightControl(() => slide('right'))}</div>}
</div>
{paginationDots()}
</div>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en_US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"live": "LIVE",
"play_item": "Play {{ title }}",
"slide_left": "Slide left",
"slide_right": "Slide right"
"slide_right": "Slide right",
"show_page": "Show page {{ number }}"
}
3 changes: 2 additions & 1 deletion src/i18n/locales/nl_NL/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"live": "",
"play_item": "",
"slide_left": "",
"slide_right": ""
"slide_right": "",
"show_page": ""
}
2 changes: 1 addition & 1 deletion src/screens/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const Home = (): JSX.Element => {

const calculateFeatured = () => {
const tilesToShow = featuredTileBreakpoints[breakpoint];
const shelfMetaHeight = 24;
const shelfMetaHeight = 36;
const shelfHorizontalMargin = isDesktop ? document.body.offsetWidth * 0.4 : 0;
const cardWidth = (document.body.offsetWidth - shelfHorizontalMargin) / tilesToShow;
const cardHeight = cardWidth * (9 / 16);
Expand Down

0 comments on commit 0317426

Please sign in to comment.