Fix visual regressions around widget permissions (#10954)

* Add a Jest snapshot of AppPermission

* Move the test inside 'for a pinned widget' category

* Make only spinner message bold

* Set font size specified with "mx_AppPermission_smallText" by default

- Add "mx_AppPermission_largeText" for elements whose size has not been specified with mx_AppPermission_smallText
- Create _AppWarning.pcss for AppWarning

* Make AppPermission panel scrollable, keeping the content at the center

* Run prettier

* Use Heading component

* Use Icon component

* Fix the test
This commit is contained in:
Suguru Hirahara 2023-06-14 11:11:06 +00:00 committed by GitHub
parent 127b542233
commit b40f29f04c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 251 additions and 70 deletions

View file

@ -21,6 +21,7 @@
@import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/elements/_AppPermission.pcss";
@import "./components/views/elements/_AppWarning.pcss";
@import "./components/views/elements/_FilterDropdown.pcss";
@import "./components/views/elements/_FilterTabGroup.pcss";
@import "./components/views/elements/_LearnMore.pcss";

View file

@ -16,41 +16,29 @@ limitations under the License.
*/
.mx_AppPermission {
> div {
margin-bottom: 12px;
}
font-size: $font-12px;
width: 100%; /* make mx_AppPermission fill width of mx_AppTileBody so that scroll bar appears on the edge */
overflow-y: scroll;
h4 {
margin: 0;
padding: 0;
}
.mx_AppPermission_content {
margin-block: auto; /* place at the center */
.mx_AppPermission_smallText {
font-size: $font-12px;
}
> div {
margin-block: 12px;
}
.mx_AppPermission_bolder {
font-weight: var(--font-semi-bold);
}
.mx_AppPermission_content_bolder {
font-weight: var(--font-semi-bold);
}
.mx_AppPermission_helpIcon {
margin-top: 1px;
margin-right: 2px;
width: 10px;
height: 10px;
display: inline-block;
&::before {
.mx_TextWithTooltip_target--helpIcon {
display: inline-block;
background-color: $accent;
mask-repeat: no-repeat;
mask-size: 12px;
width: 12px;
height: 12px;
mask-position: center;
content: "";
height: $font-14px; /* align with characters on the same line */
vertical-align: middle;
mask-image: url("$(res)/img/feather-customised/help-circle.svg");
.mx_Icon {
color: $accent;
}
}
}
}

View file

@ -0,0 +1,25 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AppWarning {
font-size: $font-16px;
justify-content: center;
h4 {
margin: 0;
padding: 0;
}
}

View file

@ -311,14 +311,7 @@ limitations under the License.
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: $font-16px;
h4 {
margin: 0;
padding: 0;
}
}
.mx_AppTile_loading {
@ -326,7 +319,6 @@ limitations under the License.
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: bold;
position: relative;
height: 100%;

View file

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<circle cx="6" cy="6" r="6"/>
<path d="M4.254 4.2a1.8 1.8 0 0 1 3.498.6c0 1.2-1.8 1.8-1.8 1.8M6 8.991"/>
</g>

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 357 B

View file

@ -25,9 +25,11 @@ import WidgetUtils from "../../../utils/WidgetUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from "../avatars/MemberAvatar";
import BaseAvatar from "../avatars/BaseAvatar";
import Heading from "../typography/Heading";
import AccessibleButton from "./AccessibleButton";
import TextWithTooltip from "./TextWithTooltip";
import { parseUrl } from "../../../utils/UrlUtils";
import { Icon as HelpIcon } from "../../../../res/img/feather-customised/help-circle.svg";
interface IProps {
url: string;
@ -117,8 +119,9 @@ export default class AppPermission extends React.Component<IProps, IState> {
<TextWithTooltip
tooltip={warningTooltipText}
tooltipClass="mx_Tooltip--appPermission mx_Tooltip--appPermission--dark"
class="mx_TextWithTooltip_target--helpIcon"
>
<span className="mx_AppPermission_helpIcon" />
<HelpIcon className="mx_Icon mx_Icon_12" />
</TextWithTooltip>
);
@ -139,20 +142,22 @@ export default class AppPermission extends React.Component<IProps, IState> {
return (
<div className="mx_AppPermission">
<div className="mx_AppPermission_bolder mx_AppPermission_smallText">{_t("Widget added by")}</div>
<div>
{avatar}
<h4 className="mx_AppPermission_bolder">{displayName}</h4>
<div className="mx_AppPermission_smallText">{userId}</div>
</div>
<div className="mx_AppPermission_smallText">{warning}</div>
<div className="mx_AppPermission_smallText">
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div>
<AccessibleButton kind="primary_sm" onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
<div className="mx_AppPermission_content">
<div className="mx_AppPermission_content_bolder">{_t("Widget added by")}</div>
<div>
{avatar}
<Heading size="h4">{displayName}</Heading>
<div>{userId}</div>
</div>
<div>{warning}</div>
<div>
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div>
<AccessibleButton kind="primary_sm" onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
</div>
</div>
);

View file

@ -62,6 +62,11 @@ jest.mock("../../../../src/stores/OwnProfileStore", () => ({
},
}));
// Fake random strings to give a predictable snapshot
jest.mock("matrix-js-sdk/src/randomstring", () => ({
randomString: () => "abdefghi",
}));
describe("AppTile", () => {
let cli: MatrixClient;
let r1: Room;
@ -387,6 +392,45 @@ describe("AppTile", () => {
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center);
});
it("should render permission request", () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = false;
}
});
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot();
expect(renderResult.queryByRole("button", { name: "Continue" })).toBeInTheDocument();
});
it("should not display 'Continue' button on permission load", () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = true;
}
});
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
});
describe("for a maximised (centered) widget", () => {
beforeEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
@ -446,21 +490,4 @@ describe("AppTile", () => {
expect(asFragment()).toMatchSnapshot();
});
});
it("for a pinned widget permission load", () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = true;
}
});
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
});
});

View file

@ -163,6 +163,149 @@ exports[`AppTile for a pinned widget should render 1`] = `
</DocumentFragment>
`;
exports[`AppTile for a pinned widget should render permission request 1`] = `
<DocumentFragment>
<div
class="mx_AppTile"
id="1"
>
<div
class="mx_AppTileMenuBar"
>
<span
class="mx_AppTileMenuBar_title"
style="pointer-events: none;"
>
<span>
<img
alt=""
class="mx_BaseAvatar mx_BaseAvatar_image mx_WidgetAvatar"
data-testid="avatar-img"
loading="lazy"
src="image-file-stub"
style="width: 20px; height: 20px;"
/>
<b>
Example 1
</b>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
title="Un-maximise"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
title="Minimise"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
title="Options"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
</span>
</div>
<div
class="mx_AppTileBody"
>
<div
class="mx_AppPermission"
>
<div
class="mx_AppPermission_content"
>
<div
class="mx_AppPermission_content_bolder"
>
Widget added by
</div>
<div>
<span
class="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 24.7px; width: 38px; line-height: 38px;"
>
U
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
loading="lazy"
src="data:image/png;base64,00"
style="width: 38px; height: 38px;"
/>
</span>
<h4
class="mx_Heading_h4"
>
@userAnother
</h4>
<div />
</div>
<div>
<span>
Using this widget may share data
<div
aria-describedby="mx_TooltipTarget_abdefghi"
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
with example.com.
</span>
</div>
<div>
This widget may use cookies. 
</div>
<div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_sm"
role="button"
tabindex="0"
>
Continue
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`AppTile preserves non-persisted widget on container move 1`] = `
<DocumentFragment>
<div