Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 39f2bba

Browse files
authored
Bring back waveform for voice messages and retain seeking (#8843)
* Crude way of layering the waveform and seek bar Not intended for production. * Use a layout prop instead of something less descriptive * Fix alignment properly, and play with styles * Convert back to a ball * Use `transparent` which makes NVDA happy enough * Allow keyboards in the seek bar * Try to make the clock behave more correctly with screen readers MIDNIGHT * Remove legacy export * Remove redundant attr * Appease the linter
1 parent d81e2ce commit 39f2bba

File tree

6 files changed

+116
-33
lines changed

6 files changed

+116
-33
lines changed

res/css/views/audio_messages/_PlaybackContainer.scss

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,57 @@ limitations under the License.
5252
padding-left: 8px; // isolate from recording circle / play control
5353
}
5454

55-
// For timeline-rendered playback, mirror the values for where the clock is in
56-
// the waveform version.
57-
.mx_SeekBar {
55+
.mx_RecordingPlayback_timelineLayoutMiddle {
5856
margin-left: 8px;
5957
margin-right: 6px;
58+
position: relative;
59+
display: inline-block;
60+
flex: 1;
61+
height: 30px; // same height as mx_Waveform, needed for automatic vertical centering
6062

63+
.mx_Waveform {
64+
position: absolute;
65+
left: 0;
66+
top: 0;
67+
}
68+
69+
.mx_SeekBar {
70+
position: absolute;
71+
left: 0;
72+
height: 30px;
73+
top: -2px; // visually vertically centered
74+
75+
// Hide the hairline progress bar since we're at 100% height. Need to have distinct rules
76+
// because CSS is weird.
77+
background: none;
78+
&::before {
79+
background: none;
80+
}
81+
&::-moz-range-progress {
82+
background: none;
83+
}
84+
85+
// Make the thumb easier to see. Like the SeekBar original styles, these need to be
86+
// distinct. We make it transparent so it doesn't show up on the UI, but also larger
87+
// so it's easier to grab by mouse users in some browsers. Most browsers let the user
88+
// move and drag the thumb regardless of hitting the thumb, however.
89+
&::-webkit-slider-thumb {
90+
width: 10px;
91+
height: 10px;
92+
background-color: transparent;
93+
}
94+
&::-moz-range-thumb {
95+
width: 10px;
96+
height: 10px;
97+
background-color: transparent;
98+
}
99+
}
100+
101+
// For timeline-rendered playback, the clock is on the other side of the waveform.
61102
& + .mx_Clock {
62103
text-align: right;
63104

64-
// Take the padding off the clock because it's accounted for in the seek bar
105+
// Take the padding off the clock because it's accounted for by the `timelineLayoutMiddle`
65106
padding: 0;
66107
}
67108
}

src/components/views/audio_messages/Clock.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React, { HTMLProps } from "react";
1818

1919
import { formatSeconds } from "../../../DateUtils";
2020

21-
export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
21+
interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
2222
seconds: number;
2323
}
2424

@@ -31,14 +31,14 @@ export default class Clock extends React.Component<IProps> {
3131
super(props);
3232
}
3333

34-
shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
34+
public shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
3535
const currentFloor = Math.floor(this.props.seconds);
3636
const nextFloor = Math.floor(nextProps.seconds);
3737
return currentFloor !== nextFloor;
3838
}
3939

4040
public render() {
41-
return <span aria-live={this.props["aria-live"]} className='mx_Clock'>
41+
return <span aria-live={this.props["aria-live"]} role={this.props.role} className='mx_Clock'>
4242
{ formatSeconds(this.props.seconds) }
4343
</span>;
4444
}

src/components/views/audio_messages/PlaybackClock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
7676
}
7777
return <Clock
7878
seconds={seconds}
79-
aria-live={this.state.playbackPhase === PlaybackState.Playing ? "off" : undefined}
79+
role="timer"
8080
/>;
8181
}
8282
}

src/components/views/audio_messages/RecordingPlayback.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,68 @@ import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerB
2222
import SeekBar from "./SeekBar";
2323
import PlaybackWaveform from "./PlaybackWaveform";
2424

25-
interface IProps extends IAudioPlayerBaseProps {
25+
export enum PlaybackLayout {
26+
/**
27+
* Clock on the left side of a waveform, without seek bar.
28+
*/
29+
Composer,
30+
2631
/**
27-
* When true, use a waveform instead of a seek bar
32+
* Clock on the right side of a waveform, with an added seek bar.
2833
*/
29-
withWaveform?: boolean;
34+
Timeline,
35+
}
36+
37+
interface IProps extends IAudioPlayerBaseProps {
38+
layout?: PlaybackLayout; // Defaults to Timeline layout
3039
}
3140

3241
export default class RecordingPlayback extends AudioPlayerBase<IProps> {
3342
// This component is rendered in two ways: the composer and timeline. They have different
3443
// rendering properties (specifically the difference of a waveform or not).
3544

36-
private renderWaveformLook(): ReactNode {
45+
private renderComposerLook(): ReactNode {
3746
return <>
3847
<PlaybackClock playback={this.props.playback} />
3948
<PlaybackWaveform playback={this.props.playback} />
4049
</>;
4150
}
4251

43-
private renderSeekableLook(): ReactNode {
52+
private renderTimelineLook(): ReactNode {
4453
return <>
45-
<SeekBar
46-
playback={this.props.playback}
47-
tabIndex={-1} // prevent tabbing into the bar
48-
playbackPhase={this.state.playbackPhase}
49-
ref={this.seekRef}
50-
/>
54+
<div className="mx_RecordingPlayback_timelineLayoutMiddle">
55+
<PlaybackWaveform playback={this.props.playback} />
56+
<SeekBar
57+
playback={this.props.playback}
58+
tabIndex={0} // allow keyboard users to fall into the seek bar
59+
playbackPhase={this.state.playbackPhase}
60+
ref={this.seekRef}
61+
/>
62+
</div>
5163
<PlaybackClock playback={this.props.playback} />
5264
</>;
5365
}
5466

5567
protected renderComponent(): ReactNode {
68+
let body: ReactNode;
69+
switch (this.props.layout) {
70+
case PlaybackLayout.Composer:
71+
body = this.renderComposerLook();
72+
break;
73+
case PlaybackLayout.Timeline: // default is timeline, fall through.
74+
default:
75+
body = this.renderTimelineLook();
76+
break;
77+
}
78+
5679
return (
5780
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer" onKeyDown={this.onKeyDown}>
5881
<PlayPauseButton
5982
playback={this.props.playback}
6083
playbackPhase={this.state.playbackPhase}
6184
ref={this.playPauseRef}
6285
/>
63-
{ this.props.withWaveform ? this.renderWaveformLook() : this.renderSeekableLook() }
86+
{ body }
6487
</div>
6588
);
6689
}

src/components/views/rooms/VoiceRecordComposerTile.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
2828
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
2929
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
3030
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
31-
import RecordingPlayback from "../audio_messages/RecordingPlayback";
31+
import RecordingPlayback, { PlaybackLayout } from "../audio_messages/RecordingPlayback";
3232
import Modal from "../../../Modal";
3333
import ErrorDialog from "../dialogs/ErrorDialog";
3434
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
@@ -231,7 +231,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
231231
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
232232

233233
if (this.state.recordingPhase !== RecordingState.Started) {
234-
return <RecordingPlayback playback={this.state.recorder.getPlayback()} withWaveform={true} />;
234+
return <RecordingPlayback
235+
playback={this.state.recorder.getPlayback()}
236+
layout={PlaybackLayout.Composer}
237+
/>;
235238
}
236239

237240
// only other UI is the recording-in-progress UI

test/components/views/audio_messages/RecordingPlayback-test.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import { mocked } from 'jest-mock';
2020
import { logger } from 'matrix-js-sdk/src/logger';
2121
import { act } from 'react-dom/test-utils';
2222

23-
import RecordingPlayback from '../../../../src/components/views/audio_messages/RecordingPlayback';
23+
import RecordingPlayback, { PlaybackLayout } from '../../../../src/components/views/audio_messages/RecordingPlayback';
2424
import { Playback } from '../../../../src/audio/Playback';
2525
import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext';
2626
import { createAudioContext } from '../../../../src/audio/compat';
2727
import { findByTestId, flushPromises } from '../../../test-utils';
2828
import PlaybackWaveform from '../../../../src/components/views/audio_messages/PlaybackWaveform';
2929
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
30+
import PlaybackClock from "../../../../src/components/views/audio_messages/PlaybackClock";
3031

3132
jest.mock('../../../../src/audio/compat', () => ({
3233
createAudioContext: jest.fn(),
@@ -128,19 +129,34 @@ describe('<RecordingPlayback />', () => {
128129
expect(playback.toggle).toHaveBeenCalled();
129130
});
130131

131-
it('should render a seek bar by default', () => {
132-
const playback = new Playback(new ArrayBuffer(8));
133-
const component = getComponent({ playback });
132+
describe('Composer Layout', () => {
133+
it('should have a waveform, no seek bar, and clock', () => {
134+
const playback = new Playback(new ArrayBuffer(8));
135+
const component = getComponent({ playback, layout: PlaybackLayout.Composer });
134136

135-
expect(component.find(PlaybackWaveform).length).toBeFalsy();
136-
expect(component.find(SeekBar).length).toBeTruthy();
137+
expect(component.find(PlaybackClock).length).toBeTruthy();
138+
expect(component.find(PlaybackWaveform).length).toBeTruthy();
139+
expect(component.find(SeekBar).length).toBeFalsy();
140+
});
137141
});
138142

139-
it('should render a waveform when requested', () => {
140-
const playback = new Playback(new ArrayBuffer(8));
141-
const component = getComponent({ playback, withWaveform: true });
143+
describe('Timeline Layout', () => {
144+
it('should have a waveform, a seek bar, and clock', () => {
145+
const playback = new Playback(new ArrayBuffer(8));
146+
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });
142147

143-
expect(component.find(PlaybackWaveform).length).toBeTruthy();
144-
expect(component.find(SeekBar).length).toBeFalsy();
148+
expect(component.find(PlaybackClock).length).toBeTruthy();
149+
expect(component.find(PlaybackWaveform).length).toBeTruthy();
150+
expect(component.find(SeekBar).length).toBeTruthy();
151+
});
152+
153+
it('should be the default', () => {
154+
const playback = new Playback(new ArrayBuffer(8));
155+
const component = getComponent({ playback }); // no layout set for test
156+
157+
expect(component.find(PlaybackClock).length).toBeTruthy();
158+
expect(component.find(PlaybackWaveform).length).toBeTruthy();
159+
expect(component.find(SeekBar).length).toBeTruthy();
160+
});
145161
});
146162
});

0 commit comments

Comments
 (0)