Implement new toast UI (#10467)

* Implement new toast UI

* Use PCSS vars and Caption component

* Add GenericToast-test

* Tweak call toast

* Fix code style
This commit is contained in:
Michael Weimann 2023-04-18 13:38:41 +02:00 committed by GitHub
parent e350b4c2c2
commit 7632f36624
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 206 additions and 66 deletions

View file

@ -14,4 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
$font-normal: 400;
$font-semi-bold: 600;

View file

@ -24,6 +24,10 @@ limitations under the License.
box-sizing: border-box;
}
.mx_Icon_secondary-content {
color: $secondary-content;
}
.mx_Icon_accent {
color: $accent;
}

View file

@ -16,10 +16,9 @@ limitations under the License.
.mx_ToastContainer {
position: absolute;
top: 0;
top: $spacing-4;
left: 70px;
z-index: 101;
padding: 4px;
display: grid;
grid-template-rows: 1fr 14px 6px;
@ -34,25 +33,29 @@ limitations under the License.
}
.mx_Toast_toast {
grid-row: 1 / 3;
grid-column: 1;
background-color: $system;
color: $primary-content;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px;
overflow: hidden;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
color: $primary-content;
column-gap: $spacing-8;
display: grid;
grid-template-columns: 22px 1fr;
column-gap: 8px;
row-gap: 4px;
padding: 8px;
grid-column: 1;
grid-row: 1 / 3;
grid-template-columns: 24px 1fr;
overflow: hidden;
padding: $spacing-16;
&.mx_Toast_hasIcon {
.mx_Toast_icon {
grid-column: 1;
grid-row: 1;
}
&::before,
&::after {
content: "";
width: 22px;
height: 22px;
width: 24px;
height: 24px;
grid-column: 1;
grid-row: 1;
mask-size: 100%;
@ -62,11 +65,6 @@ limitations under the License.
background-repeat: no-repeat;
}
&.mx_Toast_icon_verification::after {
mask-image: url("$(res)/img/e2e/normal.svg");
background-color: $primary-content;
}
&.mx_Toast_icon_verification_warning {
/* white infill for the hollow svg mask */
&::before {
@ -96,6 +94,7 @@ limitations under the License.
grid-column: 2;
}
}
&:not(.mx_Toast_hasIcon) {
padding-left: 12px;
@ -104,24 +103,19 @@ limitations under the License.
}
}
.mx_Toast_title,
.mx_Toast_description {
padding-right: 8px;
}
.mx_Toast_title {
display: flex;
align-items: center;
column-gap: 8px;
width: 100%;
box-sizing: border-box;
column-gap: 8px;
display: flex;
margin-bottom: $spacing-16;
width: 100%;
h2 {
color: $primary-content;
margin: 0;
font-size: $font-15px;
font-weight: 600;
display: inline;
width: auto;
font-size: $font-18px;
font-weight: $font-semi-bold;
}
.mx_Toast_title_countIndicator {
@ -135,25 +129,21 @@ limitations under the License.
.mx_Toast_body {
grid-column: 1 / 3;
grid-row: 2;
position: relative;
}
.mx_Toast_buttons {
column-gap: $spacing-8;
display: flex;
justify-content: flex-end;
column-gap: 5px;
.mx_AccessibleButton {
min-width: 96px;
box-sizing: border-box;
}
margin-top: $spacing-32;
}
.mx_Toast_description {
max-width: 272px;
overflow: hidden;
text-overflow: ellipsis;
margin: 4px 0 11px 0;
font-size: $font-12px;
color: $primary-content;
font-size: $font-15px;
font-weight: $font-semi-bold;
max-width: 300px;
a {
text-decoration: none;
@ -161,7 +151,10 @@ limitations under the License.
}
.mx_Toast_detail {
color: $secondary-content;
display: block;
font-weight: $font-normal;
margin-top: $spacing-4;
max-width: 300px;
}
.mx_Toast_deviceID {

View file

@ -84,20 +84,19 @@ limitations under the License.
}
.mx_IncomingLegacyCallToast_buttons {
margin-top: 8px;
display: flex;
flex-direction: row;
gap: 12px;
.mx_IncomingLegacyCallToast_button {
@mixin LegacyCallButton;
padding: 0px 8px;
flex-shrink: 0;
flex-grow: 1;
font-size: $font-15px;
span {
padding: 8px 0;
align-items: center;
display: flex;
&::before {
background-color: $button-fg-color;
content: "";
display: inline-block;
margin-right: 8px;
mask-position: center;
mask-repeat: no-repeat;
}
}
&.mx_IncomingLegacyCallToast_button_accept span::before {
@ -133,6 +132,13 @@ limitations under the License.
}
}
.mx_IncomingLegacyCallToast_silence,
.mx_IncomingLegacyCallToast_unSilence {
position: absolute;
right: 0;
top: 0;
}
.mx_IncomingLegacyCallToast_silence::before {
mask-image: url("$(res)/img/voip/silence.svg");
}

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0077 23.4869C12.0051 23.4875 12.0025 23.4881 12 23.4886C11.9975 23.4881 11.9949 23.4875 11.9923 23.4869C11.9204 23.4706 11.8129 23.4452 11.6749 23.4092C11.3989 23.3373 11.0015 23.2235 10.5233 23.0575C9.56541 22.725 8.29205 22.186 7.02249 21.3608C4.48971 19.7145 2 16.954 2 12.405V3.44957L12 0.521L22 3.44957V12.405C22 16.954 19.5103 19.7145 16.9775 21.3608C15.7079 22.186 14.4346 22.725 13.4767 23.0575C12.9985 23.2235 12.6011 23.3373 12.3251 23.4092C12.1871 23.4452 12.0796 23.4706 12.0077 23.4869Z" fill="currentColor" stroke="white"/>
<path d="M1.5 12.405V3.075L12 0L22.5 3.075V12.405C22.5 21.945 12 24 12 24C12 24 1.5 21.945 1.5 12.405Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0033 16.5C12.5774 16.5 13.0428 16.0346 13.0428 15.4605C13.0428 14.8865 12.5774 14.4211 12.0033 14.4211C11.4292 14.4211 10.9639 14.8865 10.9639 15.4605C10.9639 16.0346 11.4292 16.5 12.0033 16.5ZM10.5592 9.82291C10.5592 9.02285 11.2083 8.3792 12.0029 8.3792C12.7951 8.3792 13.4466 9.03065 13.4466 9.82291C13.4466 10.1898 13.2897 10.3208 12.6714 10.7479C12.3975 10.9372 12.0217 11.2001 11.7292 11.5858C11.416 11.9986 11.2233 12.5134 11.2233 13.168H12.7825C12.7825 12.8473 12.8677 12.6648 12.9714 12.5281C13.0957 12.3643 13.2758 12.2255 13.5577 12.0307C13.5873 12.0103 13.6185 11.989 13.6512 11.9668C14.1622 11.6192 15.0058 11.0455 15.0058 9.82291C15.0058 8.16955 13.6563 6.82001 12.0029 6.82001C10.3518 6.82001 9 8.15713 9 9.82291H10.5592Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -143,7 +143,7 @@ import { findDMForUser } from "../../utils/dm/findDMForUser";
import { Linkify } from "../../HtmlUtils";
import { NotificationColor } from "../../stores/notifications/NotificationColor";
import { UserTab } from "../views/dialogs/UserTab";
import { Icon as EncryptionIcon } from "../../../res/img/compound/encryption-24px.svg";
// legacy export
export { default as Views } from "../../Views";
@ -1669,7 +1669,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ToastStore.sharedInstance().addOrReplaceToast({
key: "verifreq_" + request.channel.transactionId,
title: _t("Verification requested"),
icon: "verification",
iconElement: (
<EncryptionIcon className="mx_Icon mx_Icon_24 mx_Icon_secondary-content mx_Toast_icon" />
),
props: { request },
component: VerificationRequestToast,
priority: 90,

View file

@ -57,10 +57,10 @@ export default class ToastContainer extends React.Component<{}, IState> {
let containerClasses;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
const { title, icon, key, component, className, bodyClassName, props } = topToast;
const { title, icon, iconElement, key, component, className, bodyClassName, props } = topToast;
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
const toastClasses = classNames("mx_Toast_toast", className, {
mx_Toast_hasIcon: icon,
mx_Toast_hasIcon: icon || iconElement,
[`mx_Toast_icon_${icon}`]: icon,
});
const toastProps = Object.assign({}, props, {
@ -86,6 +86,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
toast = (
<div className={toastClasses}>
{iconElement}
{titleElement}
<div className={bodyClasses}>{content}</div>
</div>

View file

@ -18,6 +18,7 @@ import React, { ReactNode } from "react";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
import { Caption } from "../typography/Caption";
export interface IProps {
description: ReactNode;
@ -40,7 +41,7 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
onAccept,
onReject,
}) => {
const detailContent = detail ? <div className="mx_Toast_detail">{detail}</div> : null;
const detailContent = detail ? <Caption className="mx_Toast_detail">{detail}</Caption> : null;
return (
<div>

View file

@ -19,13 +19,14 @@ import React, { HTMLAttributes } from "react";
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "className"> {
children: React.ReactNode;
className?: string;
isError?: boolean;
}
export const Caption: React.FC<Props> = ({ children, isError, ...rest }) => {
export const Caption: React.FC<Props> = ({ children, className, isError, ...rest }) => {
return (
<span
className={classNames("mx_Caption", {
className={classNames("mx_Caption", className, {
mx_Caption_error: isError,
})}
{...rest}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import EventEmitter from "events";
import React from "react";
import React, { ReactElement } from "react";
import { ComponentClass } from "../@types/common";
@ -24,7 +24,14 @@ export interface IToast<C extends ComponentClass> {
// higher priority number will be shown on top of lower priority
priority: number;
title?: string;
/**
* Icon class.
*
* @deprecated Use iconElement instead.
*/
icon?: string;
/** Icon element. Displayed left of the title. */
iconElement?: ReactElement;
component: C;
className?: string;
bodyClassName?: string;

View file

@ -119,7 +119,7 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
<div className="mx_LegacyCallEvent_type_icon" />
{isVoice ? _t("Voice call") : _t("Video call")}
</div>
<div className="mx_IncomingLegacyCallToast_buttons">
<div className="mx_Toast_buttons mx_IncomingLegacyCallToast_buttons">
<AccessibleButton
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_decline"
onClick={this.onRejectClick}

View file

@ -0,0 +1,47 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
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.
*/
import { render, RenderResult } from "@testing-library/react";
import React, { ComponentProps } from "react";
import GenericToast from "../../../../src/components/views/toasts/GenericToast";
const renderGenericToast = (props: Partial<ComponentProps<typeof GenericToast>> = {}): RenderResult => {
const propsWithDefaults = {
acceptLabel: "Accept",
description: <div>Description</div>,
onAccept: () => {},
onReject: () => {},
rejectLabel: "Reject",
...props,
};
return render(<GenericToast {...propsWithDefaults} />);
};
describe("GenericToast", () => {
it("should render as expected with detail content", () => {
const { asFragment } = renderGenericToast();
expect(asFragment()).toMatchSnapshot();
});
it("should render as expected without detail content", () => {
const { asFragment } = renderGenericToast({
detail: "Detail",
});
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GenericToast should render as expected with detail content 1`] = `
<DocumentFragment>
<div>
<div
class="mx_Toast_description"
>
<div>
Description
</div>
</div>
<div
aria-live="off"
class="mx_Toast_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
role="button"
tabindex="0"
>
Reject
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`GenericToast should render as expected without detail content 1`] = `
<DocumentFragment>
<div>
<div
class="mx_Toast_description"
>
<div>
Description
</div>
<span
class="mx_Caption mx_Toast_detail"
>
Detail
</span>
</div>
<div
aria-live="off"
class="mx_Toast_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
role="button"
tabindex="0"
>
Reject
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -27,8 +27,8 @@ exports[`UnverifiedSessionToast when rendering the toast should render as expect
<div
class="mx_Toast_description"
>
<div
class="mx_Toast_detail"
<span
class="mx_Caption mx_Toast_detail"
>
<span
data-testid="device-metadata-isVerified"
@ -41,7 +41,7 @@ exports[`UnverifiedSessionToast when rendering the toast should render as expect
>
ABC123
</span>
</div>
</span>
</div>
<div
aria-live="off"