Skip to content

Commit dfc30ac

Browse files
committed
Volume control
1 parent 02c4f96 commit dfc30ac

File tree

3 files changed

+250
-14
lines changed

3 files changed

+250
-14
lines changed

src/DefaultPlayer/Volume/Volume.css

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,74 @@
1-
.component {}
1+
.component {
2+
position: relative;
3+
}
4+
5+
.component:hover {
6+
background-color: #000;
7+
}
8+
9+
.button {
10+
width: 34px;
11+
height: 34px;
12+
background: none;
13+
border: 0;
14+
color: inherit;
15+
font: inherit;
16+
line-height: normal;
17+
overflow: visible;
18+
padding: 0;
19+
cursor: pointer;
20+
}
21+
22+
.button:focus {
23+
outline: 0;
24+
}
25+
26+
.icon {
27+
padding: 7px;
28+
}
29+
30+
.slider {
31+
display: none;
32+
position: absolute;
33+
right: 5px;
34+
bottom: 100%;
35+
left: 5px;
36+
height: 56px;
37+
background-color: #000;
38+
}
39+
40+
.component:hover .slider {
41+
display: block;
42+
}
43+
44+
.track {
45+
position: absolute;
46+
top: 8px;
47+
bottom: 8px;
48+
left: 50%;
49+
width: 4px;
50+
transform: translateX(-50%);
51+
background-color: #3e3e3e;
52+
}
53+
54+
.fill,
55+
.input {
56+
position: absolute;
57+
right: 0;
58+
bottom: 0;
59+
left: 0;
60+
height: 100%;
61+
width: 100%;
62+
}
63+
64+
.fill {
65+
background-color: #fff;
66+
}
67+
68+
.input {
69+
padding: 0;
70+
margin: 0;
71+
opacity: 0;
72+
-webkit-appearance: slider-vertical;
73+
cursor: pointer;
74+
}

src/DefaultPlayer/Volume/Volume.js

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,58 @@
11
import React from 'react';
22
import styles from './Volume.css';
3+
import VolumeOff from './../Icon/volume_off.svg';
4+
import VolumeUp from './../Icon/volume_up.svg';
35

46
export default ({ setVolume, toggleMute, volume, muted, className }) => {
57
const change = (e) => {
68
setVolume(e.target.value);
79
};
10+
const volumeValue = muted
11+
? 0
12+
: +volume;
13+
const isSilent = muted || volume <= 0;
814
return (
915
<div className={[
1016
styles.component,
1117
className
1218
].join(' ')}>
1319
<button
20+
aria-label={isSilent
21+
? 'Unmute'
22+
: 'Mute'}
23+
className={styles.button}
1424
onClick={toggleMute}
1525
type="button">
16-
{ muted || volume <= 0
17-
? 'Unmute'
18-
: 'Mute'
19-
}
26+
{ isSilent
27+
? <VolumeOff
28+
height={20}
29+
width={20}
30+
className={styles.icon}
31+
fill="#fff" />
32+
: <VolumeUp
33+
height={20}
34+
width={20}
35+
className={styles.icon}
36+
fill="#fff"/> }
2037
</button>
21-
<input
22-
min="0"
23-
step={0.1}
24-
max="1"
25-
type="range"
26-
onChange={change}
27-
value={muted
28-
? 0
29-
: +volume} />
38+
<div className={styles.slider}>
39+
<div className={styles.track}>
40+
<div
41+
className={styles.fill}
42+
style={{
43+
height: `${volumeValue * 100}%`
44+
}} />
45+
<input
46+
min="0"
47+
step={0.1}
48+
max="1"
49+
type="range"
50+
orient="vertical"
51+
onChange={change}
52+
className={styles.input}
53+
value={volumeValue} />
54+
</div>
55+
</div>
3056
</div>
3157
);
3258
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from 'react';
2+
import { shallow } from 'enzyme';
3+
import Volume from './Volume';
4+
import styles from './Volume.css';
5+
import VolumeOff from './../Icon/volume_off.svg';
6+
import VolumeUp from './../Icon/volume_up.svg';
7+
8+
describe('Volume', () => {
9+
let component;
10+
11+
beforeAll(() => {
12+
component = shallow(<Volume />);
13+
});
14+
15+
it('should accept a className prop and append it to the components class', () => {
16+
const newClassNameString = 'a new className';
17+
expect(component.prop('className'))
18+
.toContain(styles.component);
19+
component.setProps({
20+
className: newClassNameString
21+
});
22+
expect(component.prop('className'))
23+
.toContain(styles.component);
24+
expect(component.prop('className'))
25+
.toContain(newClassNameString);
26+
});
27+
28+
it('has a vertical range input with correct ranges and step', () => {
29+
const rangeInput = component.find(`.${styles.input}`);
30+
expect(rangeInput.prop('type'))
31+
.toEqual('range');
32+
expect(rangeInput.prop('orient'))
33+
.toEqual('vertical');
34+
expect(rangeInput.prop('step'))
35+
.toEqual(0.1);
36+
expect(rangeInput.prop('min'))
37+
.toEqual('0');
38+
expect(rangeInput.prop('max'))
39+
.toEqual('1');
40+
});
41+
42+
it('triggers \'toggleMute\' prop when the button is clicked', () => {
43+
const spy = jest.fn();
44+
component.setProps({
45+
toggleMute: spy
46+
});
47+
expect(spy)
48+
.not.toHaveBeenCalled();
49+
component.find('button').simulate('click');
50+
expect(spy)
51+
.toHaveBeenCalled();
52+
});
53+
54+
describe('when muted', () => {
55+
beforeAll(() => {
56+
component.setProps({
57+
muted: true,
58+
volume: 0.5
59+
});
60+
});
61+
62+
it('shows a muted icon', () => {
63+
expect(component.html())
64+
.toContain(VolumeOff);
65+
expect(component.html())
66+
.not.toContain(VolumeUp);
67+
});
68+
69+
it('has an empty track fill and range input', () => {
70+
expect(component.find(`.${styles.fill}`).prop('style').height)
71+
.toEqual('0%');
72+
expect(component.find(`.${styles.input}`).prop('value'))
73+
.toEqual(0);
74+
});
75+
76+
it('has correct aria-label', () => {
77+
expect(component.find('button').prop('aria-label'))
78+
.toEqual('Unmute');
79+
});
80+
});
81+
82+
describe('when unmuted but has no volume', () => {
83+
beforeAll(() => {
84+
component.setProps({
85+
muted: false,
86+
volume: 0
87+
});
88+
});
89+
90+
it('shows a muted icon', () => {
91+
expect(component.html())
92+
.toContain(VolumeOff);
93+
expect(component.html())
94+
.not.toContain(VolumeUp);
95+
});
96+
97+
it('has an empty track fill and range input', () => {
98+
expect(component.find(`.${styles.fill}`).prop('style').height)
99+
.toEqual('0%');
100+
expect(component.find(`.${styles.input}`).prop('value'))
101+
.toEqual(0);
102+
});
103+
104+
it('has correct aria-label', () => {
105+
expect(component.find('button').prop('aria-label'))
106+
.toEqual('Unmute');
107+
});
108+
});
109+
110+
describe('when has volume and is not muted', () => {
111+
beforeAll(() => {
112+
component.setProps({
113+
muted: false,
114+
volume: 0.5
115+
});
116+
});
117+
118+
it('shows an unmute icon', () => {
119+
expect(component.html())
120+
.toContain(VolumeUp);
121+
expect(component.html())
122+
.not.toContain(VolumeOff);
123+
});
124+
125+
it('has some track filled and a range input value', () => {
126+
expect(component.find(`.${styles.fill}`).prop('style').height)
127+
.toEqual('50%');
128+
expect(component.find(`.${styles.input}`).prop('value'))
129+
.toEqual(0.5);
130+
});
131+
132+
it('has correct aria-label', () => {
133+
expect(component.find('button').prop('aria-label'))
134+
.toEqual('Mute');
135+
});
136+
});
137+
});

0 commit comments

Comments
 (0)