Merge pull request #3775 from element-hq/toger5/new-pip-layout
Implement new Pip Layout (with control buttons)
This commit is contained in:
68
playwright/widget/pip-call-button-interaction.test.ts
Normal file
68
playwright/widget/pip-call-button-interaction.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { widgetTest } from "../fixtures/widget-user.ts";
|
||||||
|
import { HOST1, TestHelpers } from "./test-helpers.ts";
|
||||||
|
|
||||||
|
widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => {
|
||||||
|
test.skip(
|
||||||
|
browserName === "firefox",
|
||||||
|
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
|
||||||
|
);
|
||||||
|
|
||||||
|
test.slow();
|
||||||
|
|
||||||
|
const valere = await addUser("Valere", HOST1);
|
||||||
|
|
||||||
|
const callRoom = "CallRoom";
|
||||||
|
await TestHelpers.createRoom("CallRoom", valere.page);
|
||||||
|
|
||||||
|
await TestHelpers.createRoom("OtherRoom", valere.page);
|
||||||
|
|
||||||
|
await TestHelpers.switchToRoomNamed(valere.page, callRoom);
|
||||||
|
|
||||||
|
// Start the call as Valere
|
||||||
|
await TestHelpers.startCallInCurrentRoom(valere.page, false);
|
||||||
|
await expect(
|
||||||
|
valere.page.locator('iframe[title="Element Call"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await TestHelpers.joinCallFromLobby(valere.page);
|
||||||
|
// wait a bit so that the PIP has rendered
|
||||||
|
await valere.page.waitForTimeout(600);
|
||||||
|
|
||||||
|
// Switch to the other room, the call should go to PIP
|
||||||
|
await TestHelpers.switchToRoomNamed(valere.page, "OtherRoom");
|
||||||
|
|
||||||
|
// We should see the PIP overlay
|
||||||
|
const iFrame = valere.page
|
||||||
|
.locator('iframe[title="Element Call"]')
|
||||||
|
.contentFrame();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check for a bug where the video had the wrong fit in PIP
|
||||||
|
const hangupBtn = iFrame.getByRole("button", { name: "End call" });
|
||||||
|
const audioBtn = iFrame.getByTestId("incall_mute");
|
||||||
|
const videoBtn = iFrame.getByTestId("incall_videomute");
|
||||||
|
await expect(hangupBtn).toBeVisible();
|
||||||
|
await expect(audioBtn).toBeVisible();
|
||||||
|
await expect(videoBtn).toBeVisible();
|
||||||
|
await expect(audioBtn).toHaveAttribute("aria-label", /^Mute microphone$/);
|
||||||
|
await expect(videoBtn).toHaveAttribute("aria-label", /^Stop video$/);
|
||||||
|
|
||||||
|
await videoBtn.click();
|
||||||
|
await audioBtn.click();
|
||||||
|
|
||||||
|
// stop hovering on any of the buttons
|
||||||
|
await iFrame.getByTestId("videoTile").hover();
|
||||||
|
|
||||||
|
await expect(audioBtn).toHaveAttribute("aria-label", /^Unmute microphone$/);
|
||||||
|
await expect(videoBtn).toHaveAttribute("aria-label", /^Start video$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -55,7 +55,7 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => {
|
|||||||
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
|
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
|
||||||
|
|
||||||
// We should see the PIP overlay
|
// We should see the PIP overlay
|
||||||
await expect(valere.page.locator(".mx_WidgetPip_overlay")).toBeVisible();
|
await expect(valere.page.getByTestId("widget-pip-container")).toBeVisible();
|
||||||
|
|
||||||
{
|
{
|
||||||
// wait a bit so that the PIP has rendered the video
|
// wait a bit so that the PIP has rendered the video
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import styles from "./Button.module.css";
|
|||||||
|
|
||||||
interface MicButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface MicButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
size?: "sm" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MicButton: FC<MicButtonProps> = ({ muted, ...props }) => {
|
export const MicButton: FC<MicButtonProps> = ({ muted, ...props }) => {
|
||||||
@@ -47,6 +48,7 @@ export const MicButton: FC<MicButtonProps> = ({ muted, ...props }) => {
|
|||||||
|
|
||||||
interface VideoButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface VideoButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
size?: "sm" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoButton: FC<VideoButtonProps> = ({ muted, ...props }) => {
|
export const VideoButton: FC<VideoButtonProps> = ({ muted, ...props }) => {
|
||||||
@@ -71,6 +73,7 @@ export const VideoButton: FC<VideoButtonProps> = ({ muted, ...props }) => {
|
|||||||
|
|
||||||
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
size: "sm" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
|
export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
|
||||||
@@ -94,7 +97,11 @@ export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EndCallButton: FC<ComponentPropsWithoutRef<"button">> = ({
|
interface EndCallButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
|
size?: "sm" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EndCallButton: FC<EndCallButtonProps> = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
@@ -114,9 +121,10 @@ export const EndCallButton: FC<ComponentPropsWithoutRef<"button">> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsButton: FC<ComponentPropsWithoutRef<"button">> = (
|
interface SettingsButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
props,
|
size?: "sm" | "lg";
|
||||||
) => {
|
}
|
||||||
|
export const SettingsButton: FC<SettingsButtonProps> = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export function ReactionPopupMenu({
|
|||||||
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
vm: CallViewModel;
|
vm: CallViewModel;
|
||||||
|
size?: "sm" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReactionToggleButton({
|
export function ReactionToggleButton({
|
||||||
|
|||||||
@@ -108,22 +108,9 @@ Please see LICENSE in the repository root for full details.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 370px) {
|
@media (max-height: 800px) {
|
||||||
.shareScreen {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-height: 400px) {
|
|
||||||
.footer {
|
.footer {
|
||||||
display: none;
|
padding-block: var(--cpd-space-8x);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 320px) {
|
|
||||||
.invite,
|
|
||||||
.raiseHand {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +120,27 @@ Please see LICENSE in the repository root for full details.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 800px) {
|
@media (max-width: 370px) {
|
||||||
|
.shareScreen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PIP custom css */
|
||||||
|
@media (max-height: 400px) {
|
||||||
|
.shareScreen {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.footer {
|
.footer {
|
||||||
padding-block: var(--cpd-space-8x);
|
padding-block-start: var(--cpd-space-3x);
|
||||||
|
padding-block-end: var(--cpd-space-2x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
.invite,
|
||||||
|
.raiseHand {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -640,8 +640,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
|
|
||||||
|
const buttonSize = layout.type === "pip" ? "sm" : "lg";
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<MicButton
|
<MicButton
|
||||||
|
size={buttonSize}
|
||||||
key="audio"
|
key="audio"
|
||||||
muted={!audioEnabled}
|
muted={!audioEnabled}
|
||||||
onClick={toggleAudio ?? undefined}
|
onClick={toggleAudio ?? undefined}
|
||||||
@@ -649,6 +651,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
data-testid="incall_mute"
|
data-testid="incall_mute"
|
||||||
/>,
|
/>,
|
||||||
<VideoButton
|
<VideoButton
|
||||||
|
size={buttonSize}
|
||||||
key="video"
|
key="video"
|
||||||
muted={!videoEnabled}
|
muted={!videoEnabled}
|
||||||
onClick={toggleVideo ?? undefined}
|
onClick={toggleVideo ?? undefined}
|
||||||
@@ -659,6 +662,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
if (vm.toggleScreenSharing !== null) {
|
if (vm.toggleScreenSharing !== null) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ShareScreenButton
|
<ShareScreenButton
|
||||||
|
size={buttonSize}
|
||||||
key="share_screen"
|
key="share_screen"
|
||||||
className={styles.shareScreen}
|
className={styles.shareScreen}
|
||||||
enabled={sharingScreen}
|
enabled={sharingScreen}
|
||||||
@@ -670,6 +674,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
if (supportsReactions) {
|
if (supportsReactions) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ReactionToggleButton
|
<ReactionToggleButton
|
||||||
|
size={buttonSize}
|
||||||
vm={vm}
|
vm={vm}
|
||||||
key="raise_hand"
|
key="raise_hand"
|
||||||
className={styles.raiseHand}
|
className={styles.raiseHand}
|
||||||
@@ -678,10 +683,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (layout.type !== "pip")
|
if (layout.type !== "pip")
|
||||||
buttons.push(<SettingsButton key="settings" onClick={openSettings} />);
|
buttons.push(
|
||||||
|
<SettingsButton
|
||||||
|
size={buttonSize}
|
||||||
|
key="settings"
|
||||||
|
onClick={openSettings}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<EndCallButton
|
<EndCallButton
|
||||||
|
size={buttonSize}
|
||||||
key="end_call"
|
key="end_call"
|
||||||
onClick={function (): void {
|
onClick={function (): void {
|
||||||
vm.hangup();
|
vm.hangup();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import {
|
|||||||
playReactionsSound,
|
playReactionsSound,
|
||||||
showReactions,
|
showReactions,
|
||||||
} from "../../settings/settings";
|
} from "../../settings/settings";
|
||||||
import { isFirefox } from "../../Platform";
|
import { isFirefox, platform } from "../../Platform";
|
||||||
import { setPipEnabled$ } from "../../controls";
|
import { setPipEnabled$ } from "../../controls";
|
||||||
import { TileStore } from "../TileStore";
|
import { TileStore } from "../TileStore";
|
||||||
import { gridLikeLayout } from "../GridLikeLayout";
|
import { gridLikeLayout } from "../GridLikeLayout";
|
||||||
@@ -1271,7 +1271,7 @@ export function createCallViewModel$(
|
|||||||
switchMap((mode) => {
|
switchMap((mode) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "pip":
|
case "pip":
|
||||||
return of(false);
|
return of(platform === "desktop" ? true : false);
|
||||||
case "normal":
|
case "normal":
|
||||||
case "narrow":
|
case "narrow":
|
||||||
return of(true);
|
return of(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { platform } from "../Platform.ts";
|
||||||
import { type PipLayout, type PipLayoutMedia } from "./layout-types.ts";
|
import { type PipLayout, type PipLayoutMedia } from "./layout-types.ts";
|
||||||
import { type TileStore } from "./TileStore";
|
import { type TileStore } from "./TileStore";
|
||||||
|
|
||||||
@@ -16,7 +17,11 @@ export function pipLayout(
|
|||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [PipLayout, TileStore] {
|
): [PipLayout, TileStore] {
|
||||||
const update = prevTiles.from(0);
|
const update = prevTiles.from(0);
|
||||||
update.registerSpotlight(media.spotlight, true);
|
// Dont maximise in pip on EW since we want the rounded corners and the footer
|
||||||
|
update.registerSpotlight(
|
||||||
|
media.spotlight,
|
||||||
|
platform === "desktop" ? false : true,
|
||||||
|
);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user