Skip to content

Commit

Permalink
Merge pull request #2997 from metabrainz/monkey-artist-page-horizonta…
Browse files Browse the repository at this point in the history
…l-scroll

LB-1652: Better horizontal scroll on the artist pages album grids
  • Loading branch information
MonkeyDo authored Oct 11, 2024
2 parents f007f50 + b0755b5 commit 58f3d93
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 70 deletions.
18 changes: 10 additions & 8 deletions frontend/css/entity-pages.less
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,21 @@
min-height: 150px;
min-width: 0;
.artist-page& {
max-height: 520px;
max-height: 500px;
overflow-y: hidden;
border-bottom-style: inset;
position: relative;
&::after {
content: "";
background-image: @white-gradient;
position: absolute;
bottom: 0;
height: 8em;
width: 100%;
&.expanded {
max-height: initial;
padding-bottom: 4em;
}
}

.read-more {
bottom: 0;
padding: 1em 0;
}

.album-page& .listen-card .track-position {
flex: 0 3em;
align-self: center;
Expand Down
2 changes: 1 addition & 1 deletion frontend/css/release-card.less
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
}

.release-coverart.hide-image {
display: none;
display: none !important;
height: 0;
}

Expand Down
18 changes: 14 additions & 4 deletions frontend/css/scroll-container.less
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
.horizontal-scroll-container {
position: relative;

.horizontal-scroll-container.dragging .horizontal-scroll * {
// Prevent text selection and interactions when the user is actively dragging to scroll
pointer-events: none;
user-select: none;
}

.horizontal-scroll {
overflow-x: auto;
.small-scrollbar();
&.no-scrollbar {
overflow-x: scroll;
/* Hide the scrollbar: */
-ms-overflow-style: none; /* Internet Explorer and Edge */
scrollbar-width: none; /* Firefox */
Expand All @@ -23,17 +29,17 @@
border: none;
font-size: 2em;
color: @light-grey;
z-index: 1;
z-index: 2;
opacity: 1;
transition: opacity 300ms linear;
&.backward {
background: linear-gradient(to right, @white 10%, transparent);
text-align: left;
left: 0;
left: -1px;
}
&.forward {
background: linear-gradient(to left, @white 10%, transparent);
right: 0;
right: -1px;
text-align: right;
}
}
Expand All @@ -42,6 +48,10 @@
opacity: 0;
pointer-events: none;
}
// Hide arrow buttons when the container does not overflow (class added with javascript)
&.no-scroll .nav-button {
display: none;
}
}

.dragscroll {
Expand Down
27 changes: 18 additions & 9 deletions frontend/js/src/artist/ArtistPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useBrainzPlayerDispatch } from "../common/brainzplayer/BrainzPlayerCont
import SimilarArtistComponent from "../explore/music-neighborhood/components/SimilarArtist";
import CBReviewModal from "../cb-review/CBReviewModal";
import Pill from "../components/Pill";
import HorizontalScrollContainer from "../components/HorizontalScrollContainer";

function SortingButtons({
sort,
Expand Down Expand Up @@ -121,6 +122,10 @@ export default function ArtistPage(): JSX.Element {
"release_date"
);

const [expandPopularTracks, setExpandPopularTracks] = React.useState<boolean>(
false
);

const rgGroups = groupBy(
releaseGroups,
(rg) =>
Expand Down Expand Up @@ -374,7 +379,7 @@ export default function ArtistPage(): JSX.Element {
/>
</div>
<div className="entity-page-content">
<div className="tracks">
<div className={`tracks ${expandPopularTracks ? "expanded" : ""}`}>
<div className="header">
<h3 className="header-with-line">
Popular tracks
Expand Down Expand Up @@ -419,11 +424,15 @@ export default function ArtistPage(): JSX.Element {
/>
);
})}
{/* <div className="read-more">
<button type="button" className="btn btn-outline">
See more…
<div className="read-more">
<button
type="button"
className="btn btn-outline"
onClick={() => setExpandPopularTracks((prevValue) => !prevValue)}
>
See {expandPopularTracks ? "less" : "more"}
</button>
</div> */}
</div>
</div>
<div className="stats">
<div className="listening-stats card flex-center">
Expand Down Expand Up @@ -474,18 +483,18 @@ export default function ArtistPage(): JSX.Element {
)}
</div>
{Object.entries(groupedReleaseGroups).map(([type, rgGroup]) => (
<div className="albums full-width scroll-start">
<div className="albums">
<div className="listen-header">
<h3 className="header-with-line">{type}</h3>
<SortingButtons sort={sort} setSort={setSort} />
</div>
<div
className={`cover-art-container dragscroll ${
<HorizontalScrollContainer
className={`cover-art-container ${
rgGroup.length <= COVER_ART_SINGLE_ROW_COUNT ? "single-row" : ""
}`}
>
{rgGroup.map(getReleaseCard)}
</div>
</HorizontalScrollContainer>
</div>
))}
</div>
Expand Down
131 changes: 83 additions & 48 deletions frontend/js/src/components/HorizontalScrollContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type HorizontalScrollContainerProps = {
className?: string;
};

// How many pixels do the arrow buttons scroll?
const MANUAL_SCROLL_AMOUNT = 500;

export default function HorizontalScrollContainer({
showScrollbar = true,
enableDragScroll = true,
Expand All @@ -24,68 +27,100 @@ export default function HorizontalScrollContainer({
const { events } = useDraggable(scrollContainerRef, {
applyRubberBandEffect: true,
});
const { onMouseDown: draggableOnMouseDown } = events;

const onMouseDown: React.MouseEventHandler<HTMLDivElement> = (event) => {
// Call the dragScrolluse-draggable-safe hook event
events.onMouseDown(event);
// Set our own class to allow for snap-scroll
(event.target as HTMLDivElement)?.parentElement?.classList.add("dragging");
};
const onMouseUp: React.MouseEventHandler<HTMLDivElement> = (event) => {
(event.target as HTMLDivElement)?.parentElement?.classList.remove(
"dragging"
);
};
const onMouseDown: React.MouseEventHandler<HTMLElement> = React.useCallback(
(event) => {
// Call the use-draggable-scroll-safe hook event
draggableOnMouseDown(event);
// Set our own class to allow for snap-scroll
(event.target as HTMLElement)?.parentElement?.classList.add("dragging");
},
[draggableOnMouseDown]
);

const onMouseUp: React.MouseEventHandler<HTMLElement> = React.useCallback(
(event) => {
(event.target as HTMLElement)?.parentElement?.classList.remove(
"dragging"
);
},
[]
);

const onScroll: React.ReactEventHandler<HTMLDivElement> = (event) => {
const element = event.target as HTMLDivElement;
const parent = element.parentElement;
const onScroll = React.useCallback(() => {
const element = scrollContainerRef?.current;
const parent = element?.parentElement;
if (!element || !parent) {
return;
}
// calculate horizontal scroll percentage
const scrollPercentage =
(100 * element.scrollLeft) / (element.scrollWidth - element.clientWidth);
// Don't expect so big a scroll before showing nav arrows on smaller screen sizes
const requiredMinimumScrollAmount = Math.min(
MANUAL_SCROLL_AMOUNT / 2,
element.clientWidth / 2
);

if (scrollPercentage > 95) {
parent.classList.add("scroll-end");
parent.classList.remove("scroll-start");
} else if (scrollPercentage < 5) {
parent.classList.add("scroll-start");
parent.classList.remove("scroll-end");
} else {
parent.classList.remove("scroll-end");
parent.classList.remove("scroll-start");
// Set up appropriate CSS classes to show or hide nav buttons
if (element.scrollWidth <= element.clientWidth) {
parent.classList.add("no-scroll");
}
};
parent.classList.remove("scroll-end");
parent.classList.remove("scroll-start");

const manualScroll: React.ReactEventHandler<HTMLElement> = (event) => {
if (!scrollContainerRef?.current) {
return;
}
if (event?.currentTarget.classList.contains("forward")) {
scrollContainerRef.current.scrollBy({
left: 300,
top: 0,
behavior: "smooth",
});
} else {
scrollContainerRef.current.scrollBy({
left: -300,
top: 0,
behavior: "smooth",
});
if (element.scrollLeft < requiredMinimumScrollAmount) {
// We are at the beginning of the container and haven't scrolled more than requiredMinimumScrollAmount
parent.classList.add("scroll-start");
} else if (
// We have scrolled to the end of the container, i.e. there is less than requiredMinimumScrollAmount before the end of the scroll
// (with a 2px adjustement)
element.scrollWidth - element.scrollLeft - element.clientWidth <=
requiredMinimumScrollAmount - 2
) {
parent.classList.add("scroll-end");
}
};
}, []);

const throttledOnScroll = React.useMemo(
() => throttle(onScroll, 400, { leading: true }),
[onScroll]
);

const onManualScroll: React.ReactEventHandler<HTMLElement> = React.useCallback(
(event) => {
if (!scrollContainerRef?.current) {
return;
}
if (event?.currentTarget.classList.contains("forward")) {
scrollContainerRef.current.scrollBy({
left: MANUAL_SCROLL_AMOUNT,
top: 0,
behavior: "smooth",
});
} else {
scrollContainerRef.current.scrollBy({
left: -MANUAL_SCROLL_AMOUNT,
top: 0,
behavior: "smooth",
});
}
// Also call the onScroll (throttled) event to ensure
// the expected CSS classes are applied to the container
throttledOnScroll();
},
[throttledOnScroll]
);

const throttledOnScroll = throttle(onScroll, 400, { leading: true });
React.useEffect(() => {
// Run once on startup to set up expected CSS classes applied to the container
onScroll();
}, []);

return (
<div className="horizontal-scroll-container scroll-start">
<div className="horizontal-scroll-container">
<button
className="nav-button backward"
type="button"
onClick={manualScroll}
onClick={onManualScroll}
tabIndex={0}
>
<FontAwesomeIcon icon={faChevronLeft} />
Expand All @@ -106,7 +141,7 @@ export default function HorizontalScrollContainer({
<button
className="nav-button forward"
type="button"
onClick={manualScroll}
onClick={onManualScroll}
tabIndex={0}
>
<FontAwesomeIcon icon={faChevronRight} />
Expand Down

0 comments on commit 58f3d93

Please sign in to comment.