Skip to content

Commit 68d71a8

Browse files
committed
Improve interactions to hide/show the footer
This fixes a few different usability issues with the footer: - When tapping one of the footer buttons, the footer would be dismissed rather than activating the button. - When the footer was hidden, you could still tap the buttons. - Interacting with controls in the footer would not reset the timer that hides it, leading to a feeling that the footer can disappear out from under you.
1 parent 1df2e0c commit 68d71a8

File tree

3 files changed

+67
-23
lines changed

3 files changed

+67
-23
lines changed

src/room/InCallView.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ Please see LICENSE in the repository root for full details.
6666
.footer.overlay.hidden {
6767
display: grid;
6868
opacity: 0;
69-
pointer-events: none;
7069
}
7170

7271
.footer.overlay:has(:focus-visible) {

src/room/InCallView.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,22 @@ export const InCallView: FC<InCallViewProps> = ({
261261
}, [vm]);
262262
const onTouchCancel = useCallback(() => (touchStart.current = null), []);
263263

264-
// We also need to tell the layout toggle to prevent touch events from
265-
// bubbling up, or else the controls will be dismissed before a change event
266-
// can be registered on the toggle
267-
const onLayoutToggleTouchEnd = useCallback(
268-
(e: TouchEvent) => e.stopPropagation(),
269-
[],
264+
// We also need to tell the footer controls to prevent touch events from
265+
// bubbling up, or else the footer will be dismissed before a click/change
266+
// event can be registered on the control
267+
const onControlsTouchEnd = useCallback(
268+
(e: TouchEvent) => {
269+
// Somehow applying pointer-events: none to the controls when the footer
270+
// is hidden is not enough to stop clicks from happening as the footer
271+
// becomes visible, so we check manually whether the footer is shown
272+
if (showFooter) {
273+
e.stopPropagation();
274+
vm.tapControls();
275+
} else {
276+
e.preventDefault();
277+
}
278+
},
279+
[vm, showFooter],
270280
);
271281

272282
const onPointerMove = useCallback(
@@ -528,13 +538,15 @@ export const InCallView: FC<InCallViewProps> = ({
528538
key="audio"
529539
muted={!muteStates.audio.enabled}
530540
onClick={toggleMicrophone}
541+
onTouchEnd={onControlsTouchEnd}
531542
disabled={muteStates.audio.setEnabled === null}
532543
data-testid="incall_mute"
533544
/>,
534545
<VideoButton
535546
key="video"
536547
muted={!muteStates.video.enabled}
537548
onClick={toggleCamera}
549+
onTouchEnd={onControlsTouchEnd}
538550
disabled={muteStates.video.setEnabled === null}
539551
data-testid="incall_videomute"
540552
/>,
@@ -545,6 +557,7 @@ export const InCallView: FC<InCallViewProps> = ({
545557
key="switch_camera"
546558
className={styles.switchCamera}
547559
onClick={switchCamera}
560+
onTouchEnd={onControlsTouchEnd}
548561
/>,
549562
);
550563
if (canScreenshare && !hideScreensharing) {
@@ -554,6 +567,7 @@ export const InCallView: FC<InCallViewProps> = ({
554567
className={styles.shareScreen}
555568
enabled={isScreenShareEnabled}
556569
onClick={toggleScreensharing}
570+
onTouchEnd={onControlsTouchEnd}
557571
data-testid="incall_screenshare"
558572
/>,
559573
);
@@ -565,18 +579,26 @@ export const InCallView: FC<InCallViewProps> = ({
565579
className={styles.raiseHand}
566580
client={client}
567581
rtcSession={rtcSession}
582+
onTouchEnd={onControlsTouchEnd}
568583
/>,
569584
);
570585
}
571586
if (layout.type !== "pip")
572-
buttons.push(<SettingsButton key="settings" onClick={openSettings} />);
587+
buttons.push(
588+
<SettingsButton
589+
key="settings"
590+
onClick={openSettings}
591+
onTouchEnd={onControlsTouchEnd}
592+
/>,
593+
);
573594

574595
buttons.push(
575596
<EndCallButton
576597
key="end_call"
577598
onClick={function (): void {
578599
onLeave();
579600
}}
601+
onTouchEnd={onControlsTouchEnd}
580602
data-testid="incall_leave"
581603
/>,
582604
);
@@ -604,7 +626,7 @@ export const InCallView: FC<InCallViewProps> = ({
604626
className={styles.layout}
605627
layout={gridMode}
606628
setLayout={setGridMode}
607-
onTouchEnd={onLayoutToggleTouchEnd}
629+
onTouchEnd={onControlsTouchEnd}
608630
/>
609631
)}
610632
</div>

src/state/CallViewModel.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
8585
// on mobile. No spotlight tile should be shown below this threshold.
8686
const smallMobileCallThreshold = 3;
8787

88+
// How long the footer should be shown for when hovering over or interacting
89+
// with the interface
90+
const showFooterMs = 4000;
91+
8892
export interface GridLayoutMedia {
8993
type: "grid";
9094
spotlight?: MediaViewModel[];
@@ -902,6 +906,7 @@ export class CallViewModel extends ViewModel {
902906
);
903907

904908
private readonly screenTap = new Subject<void>();
909+
private readonly controlsTap = new Subject<void>();
905910
private readonly screenHover = new Subject<void>();
906911
private readonly screenUnhover = new Subject<void>();
907912

@@ -912,6 +917,13 @@ export class CallViewModel extends ViewModel {
912917
this.screenTap.next();
913918
}
914919

920+
/**
921+
* Callback for when the user taps the call's controls.
922+
*/
923+
public tapControls(): void {
924+
this.controlsTap.next();
925+
}
926+
915927
/**
916928
* Callback for when the user hovers over the call view.
917929
*/
@@ -946,27 +958,38 @@ export class CallViewModel extends ViewModel {
946958
if (isFirefox()) return of(true);
947959
// Show/hide the footer in response to interactions
948960
return merge(
949-
this.screenTap.pipe(map(() => "tap" as const)),
961+
this.screenTap.pipe(map(() => "tap screen" as const)),
962+
this.controlsTap.pipe(map(() => "tap controls" as const)),
950963
this.screenHover.pipe(map(() => "hover" as const)),
951964
).pipe(
952-
switchScan(
953-
(state, interaction) =>
954-
interaction === "tap"
955-
? state
965+
switchScan((state, interaction) => {
966+
switch (interaction) {
967+
case "tap screen":
968+
return state
956969
? // Toggle visibility on tap
957970
of(false)
958971
: // Hide after a timeout
959-
timer(6000).pipe(
972+
timer(showFooterMs).pipe(
960973
map(() => false),
961974
startWith(true),
962-
)
963-
: // Show on hover and hide after a timeout
964-
race(timer(3000), this.screenUnhover.pipe(take(1))).pipe(
965-
map(() => false),
966-
startWith(true),
967-
),
968-
false,
969-
),
975+
);
976+
case "tap controls":
977+
// The user is interacting with things, so reset the timeout
978+
return timer(showFooterMs).pipe(
979+
map(() => false),
980+
startWith(true),
981+
);
982+
case "hover":
983+
// Show on hover and hide after a timeout
984+
return race(
985+
timer(showFooterMs),
986+
this.screenUnhover.pipe(take(1)),
987+
).pipe(
988+
map(() => false),
989+
startWith(true),
990+
);
991+
}
992+
}, false),
970993
startWith(false),
971994
);
972995
}

0 commit comments

Comments
 (0)