Improve the visual experience of joining a call

Because useMeasure always returns a width and height of zero on the first render, various call UI elements would flash in and out of existence or animate in from the wrong place when joining a call. This poses an accessibility issue, and is generally unpleasant.
This commit is contained in:
Robin Townsend 2022-11-02 22:37:36 -04:00
parent db66700595
commit 2d5f413a1f
2 changed files with 91 additions and 60 deletions

View File

@ -126,6 +126,7 @@ export function InCallView({
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
const boundsValid = bounds.height > 0;
// Merge the refs so they can attach to the same element
const containerRef = useCallback(
(el: HTMLDivElement) => {
@ -238,15 +239,15 @@ export function InCallView({
const maximisedParticipant = useMemo(
() =>
fullscreenParticipant ??
(bounds.height <= 400 && bounds.width <= 400
(boundsValid && bounds.height <= 400 && bounds.width <= 400
? items.find((item) => item.focused) ??
items.find((item) => item.callFeed) ??
null
: null),
[fullscreenParticipant, bounds, items]
[fullscreenParticipant, boundsValid, bounds, items]
);
const reducedControls = bounds.width <= 400;
const reducedControls = boundsValid && bounds.width <= 400;
const renderAvatar = useCallback(
(roomMember: RoomMember, width: number, height: number) => {

View File

@ -859,73 +859,103 @@ export function VideoGrid({
});
}, [items, gridBounds, layout, isMounted, pipXRatio, pipYRatio]);
const tilePositionsValid = useRef(false);
const animate = useCallback(
(tiles: Tile[]) => (tileIndex: number) => {
const tile = tiles[tileIndex];
const tilePosition = tilePositions[tile.order];
const draggingTile = draggingTileRef.current;
const dragging = draggingTile && tile.key === draggingTile.key;
const remove = tile.remove;
(tiles: Tile[]) => {
// Whether the tile positions were valid at the time of the previous
// animation
const tilePositionsWereValid = tilePositionsValid.current;
if (dragging) {
return {
width: tilePosition.width,
height: tilePosition.height,
x: draggingTile.offsetX + draggingTile.x,
y: draggingTile.offsetY + draggingTile.y,
scale: 1.1,
opacity: 1,
zIndex: 2,
shadow: 15,
immediate: (key: string) =>
disableAnimations ||
key === "zIndex" ||
key === "x" ||
key === "y" ||
key === "shadow",
from: {
shadow: 0,
scale: 0,
opacity: 0,
},
reset: false,
};
} else {
const isMobile = isMobileBreakpoint(
gridBounds.width,
gridBounds.height
);
return (tileIndex: number) => {
const tile = tiles[tileIndex];
const tilePosition = tilePositions[tile.order];
const draggingTile = draggingTileRef.current;
const dragging = draggingTile && tile.key === draggingTile.key;
const remove = tile.remove;
tilePositionsValid.current = tilePosition.height > 0;
return {
x:
if (dragging) {
return {
width: tilePosition.width,
height: tilePosition.height,
x: draggingTile.offsetX + draggingTile.x,
y: draggingTile.offsetY + draggingTile.y,
scale: 1.1,
opacity: 1,
zIndex: 2,
shadow: 15,
immediate: (key: string) =>
disableAnimations ||
key === "zIndex" ||
key === "x" ||
key === "y" ||
key === "shadow",
from: {
shadow: 0,
scale: 0,
opacity: 0,
},
reset: false,
};
} else {
const isMobile = isMobileBreakpoint(
gridBounds.width,
gridBounds.height
);
const x =
tilePosition.x +
(layout === "spotlight" && tile.order !== 0 && isMobile
? scrollPosition
: 0),
y:
: 0);
const y =
tilePosition.y +
(layout === "spotlight" && tile.order !== 0 && !isMobile
? scrollPosition
: 0),
width: tilePosition.width,
height: tilePosition.height,
scale: remove ? 0 : 1,
opacity: remove ? 0 : 1,
zIndex: tilePosition.zIndex,
shadow: 1,
from: {
: 0);
const from: {
shadow: number;
scale: number;
opacity: number;
x?: number;
y?: number;
width?: number;
height?: number;
} = { shadow: 1, scale: 0, opacity: 0 };
let reset = false;
if (!tilePositionsWereValid) {
// This indicates that the component just mounted. We discard the
// previous keyframe by resetting the tile's position, so that it
// animates in from the right place on screen, rather than wherever
// the zero-height grid placed it.
from.x = x;
from.y = y;
from.width = tilePosition.width;
from.height = tilePosition.height;
reset = true;
}
return {
x,
y,
width: tilePosition.width,
height: tilePosition.height,
scale: remove ? 0 : 1,
opacity: remove ? 0 : 1,
zIndex: tilePosition.zIndex,
shadow: 1,
scale: 0,
opacity: 0,
},
reset: false,
immediate: (key: string) =>
disableAnimations || key === "zIndex" || key === "shadow",
// If we just stopped dragging a tile, give it time for its animation
// to settle before pushing its z-index back down
delay: (key: string) => (key === "zIndex" ? 500 : 0),
};
}
from,
reset,
immediate: (key: string) =>
disableAnimations || key === "zIndex" || key === "shadow",
// If we just stopped dragging a tile, give it time for the
// animation to settle before pushing its z-index back down
delay: (key: string) => (key === "zIndex" ? 500 : 0),
};
}
};
},
[tilePositions, disableAnimations, scrollPosition, layout, gridBounds]
);