Skip to content

Commit 60fd80e

Browse files
authored
feat: drag the progress volume bar (#22)
1 parent 52271e0 commit 60fd80e

File tree

4 files changed

+221
-95
lines changed

4 files changed

+221
-95
lines changed

src/components/controller.tsx

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
2-
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
3-
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
41
import { ReactComponent as IconMenu } from "../assets/menu.svg";
52
import { ReactComponent as IconOrderList } from "../assets/order-list.svg";
63
import { ReactComponent as IconOrderRandom } from "../assets/order-random.svg";
@@ -11,6 +8,7 @@ import { formatAudioDuration } from "../utils/formatAudioDuration";
118
import { ProgressBar } from "./progress";
129
import React, { useCallback } from "react";
1310
import { PlaylistLoop, PlaylistOrder } from "../hooks/usePlaylist";
11+
import { Volume } from "./volume";
1412

1513
type PlaybackControlsProps = {
1614
themeColor: string;
@@ -45,21 +43,6 @@ export function PlaybackControls({
4543
onLoopChange,
4644
onSeek,
4745
}: PlaybackControlsProps) {
48-
const handleVolumeBarMouseDown = useCallback(
49-
(e: React.MouseEvent<HTMLDivElement>) => {
50-
const volumeBarElement = e.currentTarget;
51-
const volumeBarRect = volumeBarElement.getBoundingClientRect();
52-
53-
onChangeVolume(
54-
Math.min(
55-
1,
56-
Math.max(0, (volumeBarRect.bottom - e.clientY) / volumeBarRect.height)
57-
)
58-
);
59-
},
60-
[onChangeVolume]
61-
);
62-
6346
// Switch order between "list" and "random"
6447
const handleOrderButtonClick = useCallback(() => {
6548
const nextOrder: PlaylistOrder = (
@@ -116,34 +99,13 @@ export function PlaybackControls({
11699
<span className="aplayer-icon aplayer-icon-back"></span>
117100
<span className="aplayer-icon aplayer-icon-play"></span>
118101
<span className="aplayer-icon aplayer-icon-forward"></span>
119-
<div className="aplayer-volume-wrap">
120-
<button
121-
className="aplayer-icon aplayer-icon-volume-down"
122-
onClick={() => onToggleMuted()}
123-
>
124-
{muted ? (
125-
<IconVolumeOff />
126-
) : volume >= 1 ? (
127-
<IconVolumeUp />
128-
) : (
129-
<IconVolumeDown />
130-
)}
131-
</button>
132-
<div
133-
className="aplayer-volume-bar-wrap"
134-
onMouseDown={handleVolumeBarMouseDown}
135-
>
136-
<div className="aplayer-volume-bar">
137-
<div
138-
className="aplayer-volume"
139-
style={{
140-
backgroundColor: themeColor,
141-
height: muted ? 0 : `${volume * 100}%`,
142-
}}
143-
></div>
144-
</div>
145-
</div>
146-
</div>
102+
<Volume
103+
themeColor={themeColor}
104+
volume={volume}
105+
muted={muted}
106+
onToggleMuted={onToggleMuted}
107+
onChangeVolume={onChangeVolume}
108+
/>
147109
<button
148110
className="aplayer-icon aplayer-icon-order"
149111
onClick={handleOrderButtonClick}

src/components/volume.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useCallback, useRef, useState } from "react";
2+
import { ReactComponent as IconVolumeUp } from "../assets/volume-up.svg";
3+
import { ReactComponent as IconVolumeDown } from "../assets/volume-down.svg";
4+
import { ReactComponent as IconVolumeOff } from "../assets/volume-off.svg";
5+
import { computePercentageOfY } from "../utils/computePercentage";
6+
import clsx from "clsx";
7+
8+
type VolumeProps = {
9+
themeColor: string;
10+
volume: number;
11+
muted: boolean;
12+
onToggleMuted: () => void;
13+
onChangeVolume: (volume: number) => void;
14+
};
15+
16+
export function Volume({
17+
themeColor,
18+
volume,
19+
muted,
20+
onToggleMuted,
21+
onChangeVolume,
22+
}: VolumeProps) {
23+
const volumeBarRef = useRef<HTMLDivElement>(null);
24+
const [isDragging, setDragging] = useState(false); // ensure related element in :hover
25+
26+
const handleMouseDown = useCallback(
27+
(e: React.MouseEvent) => {
28+
onChangeVolume(computePercentageOfY(e, volumeBarRef));
29+
setDragging(true);
30+
31+
const handleMouseMove = (e: MouseEvent) => {
32+
onChangeVolume(computePercentageOfY(e, volumeBarRef));
33+
};
34+
35+
const handleMouseUp = (e: MouseEvent) => {
36+
document.removeEventListener("mouseup", handleMouseUp);
37+
document.removeEventListener("mousemove", handleMouseMove);
38+
39+
setDragging(false);
40+
onChangeVolume(computePercentageOfY(e, volumeBarRef));
41+
};
42+
43+
document.addEventListener("mousemove", handleMouseMove);
44+
document.addEventListener("mouseup", handleMouseUp);
45+
},
46+
[onChangeVolume]
47+
);
48+
49+
return (
50+
<div className="aplayer-volume-wrap">
51+
<button
52+
className="aplayer-icon aplayer-icon-volume-down"
53+
onClick={() => onToggleMuted()}
54+
>
55+
{muted || !volume ? (
56+
<IconVolumeOff />
57+
) : volume >= 1 ? (
58+
<IconVolumeUp />
59+
) : (
60+
<IconVolumeDown />
61+
)}
62+
</button>
63+
<div
64+
className={clsx("aplayer-volume-bar-wrap", {
65+
"aplayer-volume-bar-wrap-active": isDragging,
66+
})}
67+
ref={volumeBarRef}
68+
onMouseDown={handleMouseDown}
69+
>
70+
<div className="aplayer-volume-bar">
71+
<div
72+
className="aplayer-volume"
73+
style={{
74+
backgroundColor: themeColor,
75+
height: muted ? 0 : `${volume * 100}%`,
76+
}}
77+
></div>
78+
</div>
79+
</div>
80+
</div>
81+
);
82+
}

src/utils/computePercentage.test.ts

Lines changed: 116 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,123 @@
11
import { expect, test } from "vitest";
2-
import { computePercentage } from "./computePercentage";
2+
import { computePercentage, computePercentageOfY } from "./computePercentage";
3+
import { describe } from "vitest";
34

4-
test("Return 0 if progressBarRef.current is undefined", () => {
5-
expect(
6-
computePercentage(new MouseEvent("mouseup", {}), { current: null })
7-
).toBe(0);
8-
});
5+
describe("computePercentage", () => {
6+
test("Return 0 if progressBarRef.current is undefined", () => {
7+
expect(
8+
computePercentage(new MouseEvent("mouseup", {}), { current: null })
9+
).toBe(0);
10+
});
911

10-
test("Return 0 if progressBarRef.current is undefined", () => {
11-
expect(
12-
computePercentage(new MouseEvent("mousemove"), { current: null })
13-
).toBe(0);
14-
});
12+
test("Return 0 if progressBarRef.current is undefined", () => {
13+
expect(
14+
computePercentage(new MouseEvent("mousemove"), { current: null })
15+
).toBe(0);
16+
});
17+
18+
test("Return 0 if progressBarRef.current is undefined", () => {
19+
expect(
20+
computePercentage(new MouseEvent("mousedown"), {
21+
current: null,
22+
})
23+
).toBe(0);
24+
});
25+
26+
describe("Given an valid percentage,when the mouse moves on the X axis", () => {
27+
/* MOCK DOM */
28+
test("Return valid percentage,when input two valid Event Objet", () => {
29+
const container = document.createElement("div");
30+
container.style.width = "200px";
31+
container.style.height = "2px";
32+
const mouseEvent = new MouseEvent("mousedown", {
33+
clientX: 50,
34+
clientY: 50,
35+
});
36+
37+
/* hack ! no value in the node environment , so overwrite they */
38+
container.clientWidth = 200;
39+
container.getBoundingClientRect = () => ({
40+
x: 10,
41+
y: 10,
42+
width: 300,
43+
height: 2,
44+
top: 10,
45+
right: 300,
46+
bottom: 10,
47+
left: 10,
48+
toJSON: function () {
49+
return "";
50+
},
51+
});
1552

16-
test("Return 0 if progressBarRef.current is undefined", () => {
17-
expect(
18-
computePercentage(new MouseEvent("mousedown"), {
19-
current: null,
20-
})
21-
).toBe(0);
53+
container.addEventListener("mousedown", function (e) {
54+
const val = computePercentage(e, { current: container });
55+
expect(val).toBe(0.2);
56+
});
57+
58+
container.dispatchEvent(mouseEvent);
59+
});
60+
});
2261
});
2362

24-
/* MOCK DOM */
25-
test("Return percentage when mousedown event", () => {
26-
const container = document.createElement("div");
27-
container.style.width = "200px";
28-
container.style.height = "2px";
29-
const mouseEvent = new MouseEvent("mousedown", {
30-
clientX: 50,
31-
clientY: 50,
32-
});
33-
34-
/* hack ! no value in the node environment , so overwrite they */
35-
container.clientWidth = 200;
36-
container.getBoundingClientRect = () => ({
37-
x: 10,
38-
y: 10,
39-
width: 300,
40-
height: 2,
41-
top: 10,
42-
right: 300,
43-
bottom: 10,
44-
left: 10,
45-
toJSON: function () {
46-
return "";
47-
},
48-
});
49-
50-
container.addEventListener("mousedown", function (e) {
51-
const val = computePercentage(e, { current: container });
52-
expect(val).toBe(0.2);
53-
});
54-
55-
container.dispatchEvent(mouseEvent);
63+
describe("computePercentageOfY", () => {
64+
test("Return 0 if volumeBarRef.current is undefined", () => {
65+
expect(
66+
computePercentageOfY(new MouseEvent("mouseup"), {
67+
current: null,
68+
})
69+
).toBe(0);
70+
});
71+
72+
test("Return 0 if volumeBarRef.current is undefined", () => {
73+
expect(
74+
computePercentageOfY(new MouseEvent("mousemove"), {
75+
current: null,
76+
})
77+
).toBe(0);
78+
});
79+
80+
test("Return 0 if volumeBarRef.current is undefined", () => {
81+
expect(
82+
computePercentageOfY(new MouseEvent("mousedown"), {
83+
current: null,
84+
})
85+
).toBe(0);
86+
});
87+
88+
describe("Given an valid percentage,when the mouse moves on the Y axis", () => {
89+
/* MOCK DOM */
90+
test("Return valid percentage,when input two valid Event Objet", () => {
91+
const container = document.createElement("div");
92+
container.style.width = "10px";
93+
container.style.height = "300px";
94+
const mouseEvent = new MouseEvent("mousedown", {
95+
clientX: 50,
96+
clientY: 100,
97+
});
98+
99+
/* hack ! no value in the node environment , so overwrite they */
100+
container.clientHeight = 300;
101+
container.getBoundingClientRect = () => ({
102+
x: 10,
103+
y: 10,
104+
width: 50,
105+
height: 300,
106+
top: 10,
107+
right: 300,
108+
bottom: 10,
109+
left: 10,
110+
toJSON: function () {
111+
return "";
112+
},
113+
});
114+
115+
container.addEventListener("mousedown", function (e) {
116+
const val = computePercentageOfY(e, { current: container });
117+
expect(val).toBe(0.7);
118+
});
119+
120+
container.dispatchEvent(mouseEvent);
121+
});
122+
});
56123
});

src/utils/computePercentage.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,18 @@ export function computePercentage(
1212
percentage = Math.floor(percentage * 100) / 100;
1313
return percentage;
1414
}
15+
16+
export function computePercentageOfY(
17+
eventTarget: Pick<MouseEvent, "clientY">,
18+
volumeBarRef: React.RefObject<HTMLDivElement>
19+
) {
20+
if (!volumeBarRef.current) return 0;
21+
let percentage =
22+
1 -
23+
(eventTarget.clientY - volumeBarRef.current.getBoundingClientRect().top) /
24+
volumeBarRef.current.clientHeight;
25+
percentage = Math.max(percentage, 0);
26+
percentage = Math.min(percentage, 1);
27+
percentage = Math.floor(percentage * 100) / 100;
28+
return percentage;
29+
}

0 commit comments

Comments
 (0)