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:
parent
896d890198
commit
57da29de58
9 changed files with 101 additions and 45 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue