Refactor Carousel

This commit is contained in:
Lim Chee Aun 2022-12-18 22:56:00 +08:00
parent 0b8460cd55
commit dc37100442
2 changed files with 160 additions and 116 deletions

View file

@ -2,7 +2,13 @@ import './status.css';
import { getBlurHashAverageColor } from 'fast-blurhash'; import { getBlurHashAverageColor } from 'fast-blurhash';
import mem from 'mem'; import mem from 'mem';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import useResizeObserver from 'use-resize-observer'; import useResizeObserver from 'use-resize-observer';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
@ -15,6 +21,7 @@ import htmlContentLength from '../utils/html-content-length';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import useDebouncedCallback from '../utils/useDebouncedCallback';
import visibilityIconsMap from '../utils/visibility-icons-map'; import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar'; import Avatar from './avatar';
@ -132,16 +139,6 @@ function Status({
}; };
const [showMediaModal, setShowMediaModal] = useState(false); const [showMediaModal, setShowMediaModal] = useState(false);
const carouselFocusItem = useRef(null);
const prevShowMediaModal = useRef(showMediaModal);
useEffect(() => {
if (showMediaModal !== false) {
carouselFocusItem.current?.node?.scrollIntoView({
behavior: prevShowMediaModal.current === false ? 'auto' : 'smooth',
});
}
prevShowMediaModal.current = showMediaModal;
}, [showMediaModal]);
if (reblog) { if (reblog) {
return ( return (
@ -157,7 +154,6 @@ function Status({
const [showEdited, setShowEdited] = useState(false); const [showEdited, setShowEdited] = useState(false);
const carouselRef = useRef(null);
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const spoilerContentRef = useRef(null); const spoilerContentRef = useRef(null);
@ -563,111 +559,13 @@ function Status({
</div> </div>
{showMediaModal !== false && ( {showMediaModal !== false && (
<Modal> <Modal>
<div <Carousel
ref={carouselRef} mediaAttachments={mediaAttachments}
class="carousel" index={showMediaModal}
onClick={(e) => { onClose={() => {
if ( setShowMediaModal(false);
e.target.classList.contains('carousel-item') ||
e.target.classList.contains('media')
) {
setShowMediaModal(false);
}
}} }}
tabindex="0" />
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
const rgbAverageColor = blurhash
? getBlurHashAverageColor(blurhash)
: null;
return (
<InView
class="carousel-item"
style={{
backgroundColor:
rgbAverageColor &&
`rgba(${rgbAverageColor.join(',')}, .5)`,
}}
tabindex="0"
key={media.id}
ref={i === showMediaModal ? carouselFocusItem : null}
// InView options
root={carouselRef.current}
threshold={1}
onChange={(inView) => {
if (inView) {
setShowMediaModal(i);
}
}}
>
<Media media={media} showOriginal />
</InView>
);
})}
</div>
<div class="carousel-top-controls">
<span />
<button
type="button"
class="carousel-button plain2"
onClick={() => setShowMediaModal(false)}
>
<Icon icon="x" />
</button>
</div>
{mediaAttachments?.length > 1 && (
<div class="carousel-controls">
<button
type="button"
class="carousel-button plain2"
hidden={showMediaModal === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowMediaModal(
(showMediaModal - 1 + mediaAttachments.length) %
mediaAttachments.length,
);
}}
>
<Icon icon="arrow-left" />
</button>
<span class="carousel-dots">
{mediaAttachments?.map((media, i) => (
<button
key={media.id}
type="button"
disabled={i === showMediaModal}
class={`plain carousel-dot ${
i === showMediaModal ? 'active' : ''
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowMediaModal(i);
}}
>
&bull;
</button>
))}
</span>
<button
type="button"
class="carousel-button plain2"
hidden={showMediaModal === mediaAttachments.length - 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowMediaModal(
(showMediaModal + 1) % mediaAttachments.length,
);
}}
>
<Icon icon="arrow-right" />
</button>
</div>
)}
</Modal> </Modal>
)} )}
{!!showEdited && ( {!!showEdited && (
@ -1203,4 +1101,127 @@ function StatusButton({
); );
} }
function Carousel({ mediaAttachments, index = 0, onClose = () => {} }) {
const carouselRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(index);
const carouselFocusItem = useRef(null);
useLayoutEffect(() => {
carouselFocusItem.current?.node?.scrollIntoView();
}, []);
useLayoutEffect(() => {
carouselFocusItem.current?.node?.scrollIntoView({
behavior: 'smooth',
});
}, [currentIndex]);
const onSnap = useDebouncedCallback((inView, i) => {
if (inView) {
setCurrentIndex(i);
}
}, 100);
return (
<>
<div
ref={carouselRef}
class="carousel"
onClick={(e) => {
if (
e.target.classList.contains('carousel-item') ||
e.target.classList.contains('media')
) {
onClose();
}
}}
tabindex="0"
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
const rgbAverageColor = blurhash
? getBlurHashAverageColor(blurhash)
: null;
return (
<InView
class="carousel-item"
style={{
backgroundColor:
rgbAverageColor && `rgba(${rgbAverageColor.join(',')}, .5)`,
}}
tabindex="0"
key={media.id}
ref={i === currentIndex ? carouselFocusItem : null} // InView options
root={carouselRef.current}
threshold={1}
onChange={(inView) => onSnap(inView, i)}
>
<Media media={media} showOriginal />
</InView>
);
})}
</div>
<div class="carousel-top-controls">
<span />
<button
type="button"
class="carousel-button plain2"
onClick={() => onClose()}
>
<Icon icon="x" />
</button>
</div>
{mediaAttachments?.length > 1 && (
<div class="carousel-controls">
<button
type="button"
class="carousel-button plain2"
hidden={currentIndex === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCurrentIndex(
(currentIndex - 1 + mediaAttachments.length) %
mediaAttachments.length,
);
}}
>
<Icon icon="arrow-left" />
</button>
<span class="carousel-dots">
{mediaAttachments?.map((media, i) => (
<button
key={media.id}
type="button"
disabled={i === currentIndex}
class={`plain carousel-dot ${
i === currentIndex ? 'active' : ''
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCurrentIndex(i);
}}
>
&bull;
</button>
))}
</span>
<button
type="button"
class="carousel-button plain2"
hidden={currentIndex === mediaAttachments.length - 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCurrentIndex((currentIndex + 1) % mediaAttachments.length);
}}
>
<Icon icon="arrow-right" />
</button>
</div>
)}
</>
);
}
export default Status; export default Status;

View file

@ -0,0 +1,23 @@
import { useCallback, useRef } from 'preact/hooks';
export default function useDebouncedCallback(
callback,
delay,
dependencies = [],
) {
const timeout = useRef();
const comboDeps = dependencies
? [callback, delay, ...dependencies]
: [callback, delay];
return useCallback((...args) => {
if (timeout.current != null) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => {
callback(...args);
}, delay);
}, comboDeps);
}