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

Commit 57da29d

Browse files
authored
Fix regression around CSS stacking contexts and PIP widgets (#12094)
* Fix CSS stacking contexts for Dialogs & PersistedElement Signed-off-by: Michael Telatynski <[email protected]> * Switch to PersistedElement sharing a CSS stacking context for z-index to continue functioning Signed-off-by: Michael Telatynski <[email protected]> * Fix Widget PIP overlay being under the widget and dragging being broken Signed-off-by: Michael Telatynski <[email protected]> * Fix border-radius on widget pip Signed-off-by: Michael Telatynski <[email protected]> * Fix majority of tests Signed-off-by: Michael Telatynski <[email protected]> * Fix jest retryTimes applying outside of CI Signed-off-by: Michael Telatynski <[email protected]> * Fix remaining tests Signed-off-by: Michael Telatynski <[email protected]> * Fix React unique key warnings Signed-off-by: Michael Telatynski <[email protected]> * Fix sticker picker Signed-off-by: Michael Telatynski <[email protected]> * id not class Signed-off-by: Michael Telatynski <[email protected]> * Fix widget pip button colour in light theme Signed-off-by: Michael Telatynski <[email protected]> * Revert unrelated change Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent 896d890 commit 57da29d

File tree

9 files changed

+101
-45
lines changed

9 files changed

+101
-45
lines changed

res/css/_common.pcss

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ limitations under the License.
5656
/* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */
5757
contain: strict;
5858
}
59-
.mx_Dialog_StaticContainer,
60-
.mx_Dialog_Container {
59+
#mx_ContextualMenu_Container,
60+
#mx_PersistedElement_container,
61+
#mx_Dialog_Container,
62+
#mx_Dialog_StaticContainer {
6163
/* This is required to ensure Compound tooltips correctly draw where they should with z-index: auto */
6264
isolation: isolate;
6365
}

res/css/components/views/pips/_WidgetPip.pcss

+17-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
$width: 320px;
18+
$height: 220px;
19+
1720
.mx_WidgetPip {
18-
width: 320px;
19-
height: 220px;
21+
width: $width;
22+
height: $height;
23+
}
24+
25+
.mx_WidgetPip_overlay {
26+
width: $width;
27+
height: $height;
28+
position: absolute;
29+
top: 0;
2030
border-radius: 8px;
21-
contain: paint;
31+
overflow: hidden;
2232
color: $call-primary-content;
2333
cursor: pointer;
2434
}
@@ -31,8 +41,11 @@ limitations under the License.
3141
width: 100%;
3242
box-sizing: border-box;
3343
transition: opacity ease 0.15s;
44+
}
3445

35-
.mx_WidgetPip:not(:hover) > & {
46+
.mx_WidgetPip_overlay:not(:hover) {
47+
.mx_WidgetPip_header,
48+
.mx_WidgetPip_footer {
3649
opacity: 0;
3750
}
3851
}

res/css/views/rooms/_AppsDrawer.pcss

+3
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@ limitations under the License.
325325
&.mx_AppTileBody--call {
326326
border-radius: 0px;
327327
}
328+
&.mx_AppTileBody--call.mx_AppTileBody--mini {
329+
border-radius: 8px;
330+
}
328331
}
329332

330333
/* appTileBody is embedded to PersistedElement outside of mx_AppTile,

src/components/structures/PipContainer.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
305305
const call = this.state.primaryCall;
306306
pipContent.push(({ onStartMoving, onResize }) => (
307307
<LegacyCallView
308+
key="call-view"
308309
onMouseDownOnHeader={onStartMoving}
309310
call={call}
310311
secondaryCall={this.state.secondaryCall}
@@ -317,6 +318,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
317318
if (this.state.showWidgetInPip && this.state.persistentWidgetId) {
318319
pipContent.push(({ onStartMoving }) => (
319320
<WidgetPip
321+
key="widget-pip"
320322
widgetId={this.state.persistentWidgetId!}
321323
room={MatrixClientPeg.safeGet().getRoom(this.state.persistentRoomId ?? undefined)!}
322324
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}

src/components/views/elements/AppTile.tsx

+16-11
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ interface IProps {
9393
showLayoutButtons?: boolean;
9494
// Handle to manually notify the PersistedElement that it needs to move
9595
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
96+
// An element to render after the iframe as an overlay
97+
overlay?: ReactNode;
9698
}
9799

98100
interface IState {
@@ -663,17 +665,20 @@ export default class AppTile extends React.Component<IProps, IState> {
663665
);
664666
} else {
665667
appTileBody = (
666-
<div className={appTileBodyClass} style={appTileBodyStyles}>
667-
{this.state.loading && loadingElement}
668-
<iframe
669-
title={widgetTitle}
670-
allow={iframeFeatures}
671-
ref={this.iframeRefChange}
672-
src={this.sgWidget.embedUrl}
673-
allowFullScreen={true}
674-
sandbox={sandboxFlags}
675-
/>
676-
</div>
668+
<>
669+
<div className={appTileBodyClass} style={appTileBodyStyles}>
670+
{this.state.loading && loadingElement}
671+
<iframe
672+
title={widgetTitle}
673+
allow={iframeFeatures}
674+
ref={this.iframeRefChange}
675+
src={this.sgWidget.embedUrl}
676+
allowFullScreen={true}
677+
sandbox={sandboxFlags}
678+
/>
679+
</div>
680+
{this.props.overlay}
681+
</>
677682
);
678683

679684
if (!this.props.userWidget) {

src/components/views/elements/PersistedElement.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ export const getPersistKey = (appId: string): string => "widget_" + appId;
2929
// of doing reusable widgets like dialog boxes & menus where we go and
3030
// pass in a custom control as the actual body.
3131

32+
// We contain all persisted elements within a master container to allow them all to be within the same
33+
// CSS stacking context, and thus be able to control their z-indexes relative to each other.
34+
function getOrCreateMasterContainer(): HTMLDivElement {
35+
let container = getContainer("mx_PersistedElement_container");
36+
if (!container) {
37+
container = document.createElement("div");
38+
container.id = "mx_PersistedElement_container";
39+
document.body.appendChild(container);
40+
}
41+
42+
return container;
43+
}
44+
3245
function getContainer(containerId: string): HTMLDivElement {
3346
return document.getElementById(containerId) as HTMLDivElement;
3447
}
@@ -39,7 +52,7 @@ function getOrCreateContainer(containerId: string): HTMLDivElement {
3952
if (!container) {
4053
container = document.createElement("div");
4154
container.id = containerId;
42-
document.body.appendChild(container);
55+
getOrCreateMasterContainer().appendChild(container);
4356
}
4457

4558
return container;

src/components/views/elements/PersistentApp.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export default class PersistentApp extends React.Component<IProps> {
5858
showMenubar={false}
5959
pointerEvents={this.props.pointerEvents}
6060
movePersistedElement={this.props.movePersistedElement}
61+
overlay={this.props.children}
6162
/>
6263
);
6364
}

src/components/views/pips/WidgetPip.tsx

+26-23
Original file line numberDiff line numberDiff line change
@@ -107,34 +107,37 @@ export const WidgetPip: FC<Props> = ({ widgetId, room, viewingRoom, onStartMovin
107107

108108
return (
109109
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
110-
<Toolbar className="mx_WidgetPip_header">
111-
<RovingAccessibleButton
112-
onClick={onBackClick}
113-
className="mx_WidgetPip_backButton"
114-
aria-label={_t("action|back")}
115-
>
116-
<BackIcon className="mx_Icon mx_Icon_16" />
117-
{roomName}
118-
</RovingAccessibleButton>
119-
</Toolbar>
120110
<PersistentApp
121111
persistentWidgetId={widgetId}
122112
persistentRoomId={room.roomId}
123113
pointerEvents="none"
124114
movePersistedElement={movePersistedElement}
125-
/>
126-
{(call !== null || WidgetType.JITSI.matches(widget?.type)) && (
127-
<Toolbar className="mx_WidgetPip_footer">
128-
<RovingAccessibleTooltipButton
129-
onClick={onLeaveClick}
130-
tooltip={_t("action|leave")}
131-
aria-label={_t("action|leave")}
132-
alignment={Alignment.Top}
133-
>
134-
<HangupIcon className="mx_Icon mx_Icon_24" />
135-
</RovingAccessibleTooltipButton>
136-
</Toolbar>
137-
)}
115+
>
116+
<div onMouseDown={onStartMoving} className="mx_WidgetPip_overlay">
117+
<Toolbar className="mx_WidgetPip_header">
118+
<RovingAccessibleButton
119+
onClick={onBackClick}
120+
className="mx_WidgetPip_backButton"
121+
aria-label={_t("action|back")}
122+
>
123+
<BackIcon className="mx_Icon mx_Icon_16" />
124+
{roomName}
125+
</RovingAccessibleButton>
126+
</Toolbar>
127+
{(call !== null || WidgetType.JITSI.matches(widget?.type)) && (
128+
<Toolbar className="mx_WidgetPip_footer">
129+
<RovingAccessibleTooltipButton
130+
onClick={onLeaveClick}
131+
tooltip={_t("action|leave")}
132+
aria-label={_t("action|leave")}
133+
alignment={Alignment.Top}
134+
>
135+
<HangupIcon className="mx_Icon mx_Icon_24" />
136+
</RovingAccessibleTooltipButton>
137+
</Toolbar>
138+
)}
139+
</div>
140+
</PersistentApp>
138141
</div>
139142
);
140143
};

test/components/structures/PipContainer-test.tsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ import { WidgetType } from "../../../src/widgets/WidgetType";
6464
import { SdkContextClass } from "../../../src/contexts/SDKContext";
6565
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
6666

67+
jest.mock("../../../src/stores/OwnProfileStore", () => ({
68+
OwnProfileStore: {
69+
instance: {
70+
isProfileInfoFetched: true,
71+
removeListener: jest.fn(),
72+
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
73+
},
74+
},
75+
}));
76+
6777
describe("PipContainer", () => {
6878
useMockedCalls();
6979
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
@@ -91,6 +101,8 @@ describe("PipContainer", () => {
91101

92102
stubClient();
93103
client = mocked(MatrixClientPeg.safeGet());
104+
client.getUserId.mockReturnValue("@alice:example.org");
105+
client.getSafeUserId.mockReturnValue("@alice:example.org");
94106
DMRoomMap.makeShared(client);
95107

96108
room = new Room("!1:example.org", client, "@alice:example.org", {
@@ -161,6 +173,7 @@ describe("PipContainer", () => {
161173
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
162174

163175
const widget = new Widget(call.widget);
176+
WidgetStore.instance.addVirtualWidget(call.widget, room.roomId);
164177
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
165178
stop: () => {},
166179
} as unknown as ClientWidgetApi);
@@ -175,6 +188,7 @@ describe("PipContainer", () => {
175188
cleanup();
176189
call.destroy();
177190
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
191+
WidgetStore.instance.removeVirtualWidget(widget.id, room.roomId);
178192
};
179193

180194
const withWidget = async (fn: () => Promise<void>): Promise<void> => {
@@ -265,7 +279,7 @@ describe("PipContainer", () => {
265279
const widget = WidgetStore.instance.addVirtualWidget(
266280
{
267281
id: "1",
268-
creatorUserId: "@alice:exaxmple.org",
282+
creatorUserId: "@alice:example.org",
269283
type: WidgetType.CUSTOM.preferred,
270284
url: "https://example.org",
271285
name: "Example widget",
@@ -279,7 +293,7 @@ describe("PipContainer", () => {
279293

280294
// The return button should maximize the widget
281295
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
282-
await user.click(screen.getByRole("button", { name: "Back" }));
296+
await user.click(await screen.findByRole("button", { name: "Back" }));
283297
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
284298

285299
expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
@@ -295,7 +309,7 @@ describe("PipContainer", () => {
295309
const widget = WidgetStore.instance.addVirtualWidget(
296310
{
297311
id: "1",
298-
creatorUserId: "@alice:exaxmple.org",
312+
creatorUserId: "@alice:example.org",
299313
type: WidgetType.JITSI.preferred,
300314
url: "https://meet.example.org",
301315
name: "Jitsi example",
@@ -310,7 +324,7 @@ describe("PipContainer", () => {
310324
// The return button should view the room
311325
const dispatcherSpy = jest.fn();
312326
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
313-
await user.click(screen.getByRole("button", { name: "Back" }));
327+
await user.click(await screen.findByRole("button", { name: "Back" }));
314328
expect(dispatcherSpy).toHaveBeenCalledWith({
315329
action: Action.ViewRoom,
316330
room_id: room.roomId,

0 commit comments

Comments
 (0)