Fix regression around CSS stacking contexts and PIP widgets (#12094)

* Fix CSS stacking contexts for Dialogs & PersistedElement

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to PersistedElement sharing a CSS stacking context for z-index to continue functioning

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix Widget PIP overlay being under the widget and dragging being broken

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix border-radius on widget pip

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix majority of tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix jest retryTimes applying outside of CI

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix remaining tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix React unique key warnings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix sticker picker

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* id not class

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix widget pip button colour in light theme

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert unrelated change

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-01-08 12:11:44 +00:00 committed by GitHub
parent 896d890198
commit 57da29de58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 101 additions and 45 deletions

View file

@ -56,8 +56,10 @@ limitations under the License.
/* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */ /* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */
contain: strict; contain: strict;
} }
.mx_Dialog_StaticContainer, #mx_ContextualMenu_Container,
.mx_Dialog_Container { #mx_PersistedElement_container,
#mx_Dialog_Container,
#mx_Dialog_StaticContainer {
/* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */ /* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */
isolation: isolate; isolation: isolate;
} }

View file

@ -14,11 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
$width: 320px;
$height: 220px;
.mx_WidgetPip { .mx_WidgetPip {
width: 320px; width: $width;
height: 220px; height: $height;
}
.mx_WidgetPip_overlay {
width: $width;
height: $height;
position: absolute;
top: 0;
border-radius: 8px; border-radius: 8px;
contain: paint; overflow: hidden;
color: $call-primary-content; color: $call-primary-content;
cursor: pointer; cursor: pointer;
} }
@ -31,8 +41,11 @@ limitations under the License.
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
transition: opacity ease 0.15s; transition: opacity ease 0.15s;
}
.mx_WidgetPip:not(:hover) > & { .mx_WidgetPip_overlay:not(:hover) {
.mx_WidgetPip_header,
.mx_WidgetPip_footer {
opacity: 0; opacity: 0;
} }
} }

View file

@ -325,6 +325,9 @@ limitations under the License.
&.mx_AppTileBody--call { &.mx_AppTileBody--call {
border-radius: 0px; border-radius: 0px;
} }
&.mx_AppTileBody--call.mx_AppTileBody--mini {
border-radius: 8px;
}
} }
/* appTileBody is embedded to PersistedElement outside of mx_AppTile, /* appTileBody is embedded to PersistedElement outside of mx_AppTile,

View file

@ -305,6 +305,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
const call = this.state.primaryCall; const call = this.state.primaryCall;
pipContent.push(({ onStartMoving, onResize }) => ( pipContent.push(({ onStartMoving, onResize }) => (
<LegacyCallView <LegacyCallView
key="call-view"
onMouseDownOnHeader={onStartMoving} onMouseDownOnHeader={onStartMoving}
call={call} call={call}
secondaryCall={this.state.secondaryCall} secondaryCall={this.state.secondaryCall}
@ -317,6 +318,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
if (this.state.showWidgetInPip && this.state.persistentWidgetId) { if (this.state.showWidgetInPip && this.state.persistentWidgetId) {
pipContent.push(({ onStartMoving }) => ( pipContent.push(({ onStartMoving }) => (
<WidgetPip <WidgetPip
key="widget-pip"
widgetId={this.state.persistentWidgetId!} widgetId={this.state.persistentWidgetId!}
room={MatrixClientPeg.safeGet().getRoom(this.state.persistentRoomId ?? undefined)!} room={MatrixClientPeg.safeGet().getRoom(this.state.persistentRoomId ?? undefined)!}
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId} viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}

View file

@ -93,6 +93,8 @@ interface IProps {
showLayoutButtons?: boolean; showLayoutButtons?: boolean;
// Handle to manually notify the PersistedElement that it needs to move // Handle to manually notify the PersistedElement that it needs to move
movePersistedElement?: MutableRefObject<(() => void) | undefined>; movePersistedElement?: MutableRefObject<(() => void) | undefined>;
// An element to render after the iframe as an overlay
overlay?: ReactNode;
} }
interface IState { interface IState {
@ -663,17 +665,20 @@ export default class AppTile extends React.Component<IProps, IState> {
); );
} else { } else {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <>
{this.state.loading && loadingElement} <div className={appTileBodyClass} style={appTileBodyStyles}>
<iframe {this.state.loading && loadingElement}
title={widgetTitle} <iframe
allow={iframeFeatures} title={widgetTitle}
ref={this.iframeRefChange} allow={iframeFeatures}
src={this.sgWidget.embedUrl} ref={this.iframeRefChange}
allowFullScreen={true} src={this.sgWidget.embedUrl}
sandbox={sandboxFlags} allowFullScreen={true}
/> sandbox={sandboxFlags}
</div> />
</div>
{this.props.overlay}
</>
); );
if (!this.props.userWidget) { if (!this.props.userWidget) {

View file

@ -29,6 +29,19 @@ export const getPersistKey = (appId: string): string => "widget_" + appId;
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body. // pass in a custom control as the actual body.
// We contain all persisted elements within a master container to allow them all to be within the same
// CSS stacking context, and thus be able to control their z-indexes relative to each other.
function getOrCreateMasterContainer(): HTMLDivElement {
let container = getContainer("mx_PersistedElement_container");
if (!container) {
container = document.createElement("div");
container.id = "mx_PersistedElement_container";
document.body.appendChild(container);
}
return container;
}
function getContainer(containerId: string): HTMLDivElement { function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId) as HTMLDivElement; return document.getElementById(containerId) as HTMLDivElement;
} }
@ -39,7 +52,7 @@ function getOrCreateContainer(containerId: string): HTMLDivElement {
if (!container) { if (!container) {
container = document.createElement("div"); container = document.createElement("div");
container.id = containerId; container.id = containerId;
document.body.appendChild(container); getOrCreateMasterContainer().appendChild(container);
} }
return container; return container;

View file

@ -58,6 +58,7 @@ export default class PersistentApp extends React.Component<IProps> {
showMenubar={false} showMenubar={false}
pointerEvents={this.props.pointerEvents} pointerEvents={this.props.pointerEvents}
movePersistedElement={this.props.movePersistedElement} movePersistedElement={this.props.movePersistedElement}
overlay={this.props.children}
/> />
); );
} }

View file

@ -107,34 +107,37 @@ export const WidgetPip: FC<Props> = ({ widgetId, room, viewingRoom, onStartMovin
return ( return (
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}> <div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
<Toolbar className="mx_WidgetPip_header">
<RovingAccessibleButton
onClick={onBackClick}
className="mx_WidgetPip_backButton"
aria-label={_t("action|back")}
>
<BackIcon className="mx_Icon mx_Icon_16" />
{roomName}
</RovingAccessibleButton>
</Toolbar>
<PersistentApp <PersistentApp
persistentWidgetId={widgetId} persistentWidgetId={widgetId}
persistentRoomId={room.roomId} persistentRoomId={room.roomId}
pointerEvents="none" pointerEvents="none"
movePersistedElement={movePersistedElement} movePersistedElement={movePersistedElement}
/> >
{(call !== null || WidgetType.JITSI.matches(widget?.type)) && ( <div onMouseDown={onStartMoving} className="mx_WidgetPip_overlay">
<Toolbar className="mx_WidgetPip_footer"> <Toolbar className="mx_WidgetPip_header">
<RovingAccessibleTooltipButton <RovingAccessibleButton
onClick={onLeaveClick} onClick={onBackClick}
tooltip={_t("action|leave")} className="mx_WidgetPip_backButton"
aria-label={_t("action|leave")} aria-label={_t("action|back")}
alignment={Alignment.Top} >
> <BackIcon className="mx_Icon mx_Icon_16" />
<HangupIcon className="mx_Icon mx_Icon_24" /> {roomName}
</RovingAccessibleTooltipButton> </RovingAccessibleButton>
</Toolbar> </Toolbar>
)} {(call !== null || WidgetType.JITSI.matches(widget?.type)) && (
<Toolbar className="mx_WidgetPip_footer">
<RovingAccessibleTooltipButton
onClick={onLeaveClick}
tooltip={_t("action|leave")}
aria-label={_t("action|leave")}
alignment={Alignment.Top}
>
<HangupIcon className="mx_Icon mx_Icon_24" />
</RovingAccessibleTooltipButton>
</Toolbar>
)}
</div>
</PersistentApp>
</div> </div>
); );
}; };

View file

@ -64,6 +64,16 @@ import { WidgetType } from "../../../src/widgets/WidgetType";
import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions"; import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
jest.mock("../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
isProfileInfoFetched: true,
removeListener: jest.fn(),
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
},
},
}));
describe("PipContainer", () => { describe("PipContainer", () => {
useMockedCalls(); useMockedCalls();
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {}); jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
@ -91,6 +101,8 @@ describe("PipContainer", () => {
stubClient(); stubClient();
client = mocked(MatrixClientPeg.safeGet()); client = mocked(MatrixClientPeg.safeGet());
client.getUserId.mockReturnValue("@alice:example.org");
client.getSafeUserId.mockReturnValue("@alice:example.org");
DMRoomMap.makeShared(client); DMRoomMap.makeShared(client);
room = new Room("!1:example.org", client, "@alice:example.org", { room = new Room("!1:example.org", client, "@alice:example.org", {
@ -161,6 +173,7 @@ describe("PipContainer", () => {
if (!(call instanceof MockedCall)) throw new Error("Failed to create call"); if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
const widget = new Widget(call.widget); const widget = new Widget(call.widget);
WidgetStore.instance.addVirtualWidget(call.widget, room.roomId);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, { WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {}, stop: () => {},
} as unknown as ClientWidgetApi); } as unknown as ClientWidgetApi);
@ -175,6 +188,7 @@ describe("PipContainer", () => {
cleanup(); cleanup();
call.destroy(); call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId); ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
WidgetStore.instance.removeVirtualWidget(widget.id, room.roomId);
}; };
const withWidget = async (fn: () => Promise<void>): Promise<void> => { const withWidget = async (fn: () => Promise<void>): Promise<void> => {
@ -265,7 +279,7 @@ describe("PipContainer", () => {
const widget = WidgetStore.instance.addVirtualWidget( const widget = WidgetStore.instance.addVirtualWidget(
{ {
id: "1", id: "1",
creatorUserId: "@alice:exaxmple.org", creatorUserId: "@alice:example.org",
type: WidgetType.CUSTOM.preferred, type: WidgetType.CUSTOM.preferred,
url: "https://example.org", url: "https://example.org",
name: "Example widget", name: "Example widget",
@ -279,7 +293,7 @@ describe("PipContainer", () => {
// The return button should maximize the widget // The return button should maximize the widget
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer"); const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await user.click(screen.getByRole("button", { name: "Back" })); await user.click(await screen.findByRole("button", { name: "Back" }));
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center); expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
expect(screen.queryByRole("button", { name: "Leave" })).toBeNull(); expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
@ -295,7 +309,7 @@ describe("PipContainer", () => {
const widget = WidgetStore.instance.addVirtualWidget( const widget = WidgetStore.instance.addVirtualWidget(
{ {
id: "1", id: "1",
creatorUserId: "@alice:exaxmple.org", creatorUserId: "@alice:example.org",
type: WidgetType.JITSI.preferred, type: WidgetType.JITSI.preferred,
url: "https://meet.example.org", url: "https://meet.example.org",
name: "Jitsi example", name: "Jitsi example",
@ -310,7 +324,7 @@ describe("PipContainer", () => {
// The return button should view the room // The return button should view the room
const dispatcherSpy = jest.fn(); const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy); const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
await user.click(screen.getByRole("button", { name: "Back" })); await user.click(await screen.findByRole("button", { name: "Back" }));
expect(dispatcherSpy).toHaveBeenCalledWith({ expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: room.roomId, room_id: room.roomId,