Skip to content

Commit

Permalink
feat: drag the progress volume bar (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
raotaohub authored Jun 6, 2023
1 parent 52271e0 commit 60fd80e
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 95 deletions.
54 changes: 8 additions & 46 deletions src/components/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
import { ReactComponent as IconMenu } from "../assets/menu.svg";
import { ReactComponent as IconOrderList } from "../assets/order-list.svg";
import { ReactComponent as IconOrderRandom } from "../assets/order-random.svg";
Expand All @@ -11,6 +8,7 @@ import { formatAudioDuration } from "../utils/formatAudioDuration";
import { ProgressBar } from "./progress";
import React, { useCallback } from "react";
import { PlaylistLoop, PlaylistOrder } from "../hooks/usePlaylist";
import { Volume } from "./volume";

type PlaybackControlsProps = {
themeColor: string;
Expand Down Expand Up @@ -45,21 +43,6 @@ export function PlaybackControls({
onLoopChange,
onSeek,
}: PlaybackControlsProps) {
const handleVolumeBarMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const volumeBarElement = e.currentTarget;
const volumeBarRect = volumeBarElement.getBoundingClientRect();

onChangeVolume(
Math.min(
1,
Math.max(0, (volumeBarRect.bottom - e.clientY) / volumeBarRect.height)
)
);
},
[onChangeVolume]
);

// Switch order between "list" and "random"
const handleOrderButtonClick = useCallback(() => {
const nextOrder: PlaylistOrder = (
Expand Down Expand Up @@ -116,34 +99,13 @@ export function PlaybackControls({
<span className="aplayer-icon aplayer-icon-back"></span>
<span className="aplayer-icon aplayer-icon-play"></span>
<span className="aplayer-icon aplayer-icon-forward"></span>
<div className="aplayer-volume-wrap">
<button
className="aplayer-icon aplayer-icon-volume-down"
onClick={() => onToggleMuted()}
>
{muted ? (
<IconVolumeOff />
) : volume >= 1 ? (
<IconVolumeUp />
) : (
<IconVolumeDown />
)}
</button>
<div
className="aplayer-volume-bar-wrap"
onMouseDown={handleVolumeBarMouseDown}
>
<div className="aplayer-volume-bar">
<div
className="aplayer-volume"
style={{
backgroundColor: themeColor,
height: muted ? 0 : `${volume * 100}%`,
}}
></div>
</div>
</div>
</div>
<Volume
themeColor={themeColor}
volume={volume}
muted={muted}
onToggleMuted={onToggleMuted}
onChangeVolume={onChangeVolume}
/>
<button
className="aplayer-icon aplayer-icon-order"
onClick={handleOrderButtonClick}
Expand Down
82 changes: 82 additions & 0 deletions src/components/volume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useCallback, useRef, useState } from "react";
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
import { computePercentageOfY } from "../utils/computePercentage";
import clsx from "clsx";

type VolumeProps = {
themeColor: string;
volume: number;
muted: boolean;
onToggleMuted: () => void;
onChangeVolume: (volume: number) => void;
};

export function Volume({
themeColor,
volume,
muted,
onToggleMuted,
onChangeVolume,
}: VolumeProps) {
const volumeBarRef = useRef<HTMLDivElement>(null);
const [isDragging, setDragging] = useState(false); // ensure related element in :hover

const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
onChangeVolume(computePercentageOfY(e, volumeBarRef));
setDragging(true);

const handleMouseMove = (e: MouseEvent) => {
onChangeVolume(computePercentageOfY(e, volumeBarRef));
};

const handleMouseUp = (e: MouseEvent) => {
document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("mousemove", handleMouseMove);

setDragging(false);
onChangeVolume(computePercentageOfY(e, volumeBarRef));
};

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[onChangeVolume]
);

return (
<div className="aplayer-volume-wrap">
<button
className="aplayer-icon aplayer-icon-volume-down"
onClick={() => onToggleMuted()}
>
{muted || !volume ? (
<IconVolumeOff />
) : volume >= 1 ? (
<IconVolumeUp />
) : (
<IconVolumeDown />
)}
</button>
<div
className={clsx("aplayer-volume-bar-wrap", {
"aplayer-volume-bar-wrap-active": isDragging,
})}
ref={volumeBarRef}
onMouseDown={handleMouseDown}
>
<div className="aplayer-volume-bar">
<div
className="aplayer-volume"
style={{
backgroundColor: themeColor,
height: muted ? 0 : `${volume * 100}%`,
}}
></div>
</div>
</div>
</div>
);
}
165 changes: 116 additions & 49 deletions src/utils/computePercentage.test.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,123 @@
import { expect, test } from "vitest";
import { computePercentage } from "./computePercentage";
import { computePercentage, computePercentageOfY } from "./computePercentage";
import { describe } from "vitest";

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mouseup", {}), { current: null })
).toBe(0);
});
describe("computePercentage", () => {
test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mouseup", {}), { current: null })
).toBe(0);
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousemove"), { current: null })
).toBe(0);
});
test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousemove"), { current: null })
).toBe(0);
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousedown"), {
current: null,
})
).toBe(0);
});

describe("Given an valid percentage,when the mouse moves on the X axis", () => {
/* MOCK DOM */
test("Return valid percentage,when input two valid Event Objet", () => {
const container = document.createElement("div");
container.style.width = "200px";
container.style.height = "2px";
const mouseEvent = new MouseEvent("mousedown", {
clientX: 50,
clientY: 50,
});

/* hack ! no value in the node environment , so overwrite they */
container.clientWidth = 200;
container.getBoundingClientRect = () => ({
x: 10,
y: 10,
width: 300,
height: 2,
top: 10,
right: 300,
bottom: 10,
left: 10,
toJSON: function () {
return "";
},
});

test("Return 0 if progressBarRef.current is undefined", () => {
expect(
computePercentage(new MouseEvent("mousedown"), {
current: null,
})
).toBe(0);
container.addEventListener("mousedown", function (e) {
const val = computePercentage(e, { current: container });
expect(val).toBe(0.2);
});

container.dispatchEvent(mouseEvent);
});
});
});

/* MOCK DOM */
test("Return percentage when mousedown event", () => {
const container = document.createElement("div");
container.style.width = "200px";
container.style.height = "2px";
const mouseEvent = new MouseEvent("mousedown", {
clientX: 50,
clientY: 50,
});

/* hack ! no value in the node environment , so overwrite they */
container.clientWidth = 200;
container.getBoundingClientRect = () => ({
x: 10,
y: 10,
width: 300,
height: 2,
top: 10,
right: 300,
bottom: 10,
left: 10,
toJSON: function () {
return "";
},
});

container.addEventListener("mousedown", function (e) {
const val = computePercentage(e, { current: container });
expect(val).toBe(0.2);
});

container.dispatchEvent(mouseEvent);
describe("computePercentageOfY", () => {
test("Return 0 if volumeBarRef.current is undefined", () => {
expect(
computePercentageOfY(new MouseEvent("mouseup"), {
current: null,
})
).toBe(0);
});

test("Return 0 if volumeBarRef.current is undefined", () => {
expect(
computePercentageOfY(new MouseEvent("mousemove"), {
current: null,
})
).toBe(0);
});

test("Return 0 if volumeBarRef.current is undefined", () => {
expect(
computePercentageOfY(new MouseEvent("mousedown"), {
current: null,
})
).toBe(0);
});

describe("Given an valid percentage,when the mouse moves on the Y axis", () => {
/* MOCK DOM */
test("Return valid percentage,when input two valid Event Objet", () => {
const container = document.createElement("div");
container.style.width = "10px";
container.style.height = "300px";
const mouseEvent = new MouseEvent("mousedown", {
clientX: 50,
clientY: 100,
});

/* hack ! no value in the node environment , so overwrite they */
container.clientHeight = 300;
container.getBoundingClientRect = () => ({
x: 10,
y: 10,
width: 50,
height: 300,
top: 10,
right: 300,
bottom: 10,
left: 10,
toJSON: function () {
return "";
},
});

container.addEventListener("mousedown", function (e) {
const val = computePercentageOfY(e, { current: container });
expect(val).toBe(0.7);
});

container.dispatchEvent(mouseEvent);
});
});
});
15 changes: 15 additions & 0 deletions src/utils/computePercentage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ export function computePercentage(
percentage = Math.floor(percentage * 100) / 100;
return percentage;
}

export function computePercentageOfY(
eventTarget: Pick<MouseEvent, "clientY">,
volumeBarRef: React.RefObject<HTMLDivElement>
) {
if (!volumeBarRef.current) return 0;
let percentage =
1 -
(eventTarget.clientY - volumeBarRef.current.getBoundingClientRect().top) /
volumeBarRef.current.clientHeight;
percentage = Math.max(percentage, 0);
percentage = Math.min(percentage, 1);
percentage = Math.floor(percentage * 100) / 100;
return percentage;
}

1 comment on commit 60fd80e

@vercel
Copy link

@vercel vercel bot commented on 60fd80e Jun 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.