From 7632f366248187f094acb44623a8ec76e562ed87 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 18 Apr 2023 13:38:41 +0200 Subject: [PATCH] Implement new toast UI (#10467) * Implement new toast UI * Use PCSS vars and Caption component * Add GenericToast-test * Tweak call toast * Fix code style --- res/css/_font-weights.pcss | 1 + res/css/compound/_Icon.pcss | 4 + res/css/structures/_ToastContainer.pcss | 77 +++++++++---------- .../toasts/_IncomingLegacyCallToast.pcss | 30 +++++--- res/img/compound/encryption-24px.svg | 5 ++ src/components/structures/MatrixChat.tsx | 6 +- src/components/structures/ToastContainer.tsx | 5 +- src/components/views/toasts/GenericToast.tsx | 3 +- src/components/views/typography/Caption.tsx | 5 +- src/stores/ToastStore.ts | 9 ++- src/toasts/IncomingLegacyCallToast.tsx | 2 +- .../views/toasts/GenericToast-test.tsx | 47 +++++++++++ .../__snapshots__/GenericToast-test.tsx.snap | 72 +++++++++++++++++ .../UnverifiedSessionToast-test.tsx.snap | 6 +- 14 files changed, 206 insertions(+), 66 deletions(-) create mode 100644 res/img/compound/encryption-24px.svg create mode 100644 test/components/views/toasts/GenericToast-test.tsx create mode 100644 test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap diff --git a/res/css/_font-weights.pcss b/res/css/_font-weights.pcss index 3e2b19d516..6999aa3150 100644 --- a/res/css/_font-weights.pcss +++ b/res/css/_font-weights.pcss @@ -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; diff --git a/res/css/compound/_Icon.pcss b/res/css/compound/_Icon.pcss index 07f9eb5a0e..5edac93a7b 100644 --- a/res/css/compound/_Icon.pcss +++ b/res/css/compound/_Icon.pcss @@ -24,6 +24,10 @@ limitations under the License. box-sizing: border-box; } +.mx_Icon_secondary-content { + color: $secondary-content; +} + .mx_Icon_accent { color: $accent; } diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 9a1e3b5e19..65a92e552f 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -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 { diff --git a/res/css/views/toasts/_IncomingLegacyCallToast.pcss b/res/css/views/toasts/_IncomingLegacyCallToast.pcss index e2092ef006..0306aba8ac 100644 --- a/res/css/views/toasts/_IncomingLegacyCallToast.pcss +++ b/res/css/views/toasts/_IncomingLegacyCallToast.pcss @@ -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"); } diff --git a/res/img/compound/encryption-24px.svg b/res/img/compound/encryption-24px.svg new file mode 100644 index 0000000000..c5f8b67fca --- /dev/null +++ b/res/img/compound/encryption-24px.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 575ff07925..a503895058 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -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 { ToastStore.sharedInstance().addOrReplaceToast({ key: "verifreq_" + request.channel.transactionId, title: _t("Verification requested"), - icon: "verification", + iconElement: ( + + ), props: { request }, component: VerificationRequestToast, priority: 90, diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index c95f5a1099..d829adfa80 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -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 = (
+ {iconElement} {titleElement}
{content}
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index c40eaab8e8..8808ee6801 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -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> = ({ onAccept, onReject, }) => { - const detailContent = detail ?
{detail}
: null; + const detailContent = detail ? {detail} : null; return (
diff --git a/src/components/views/typography/Caption.tsx b/src/components/views/typography/Caption.tsx index 69e7714b22..89fd551ed5 100644 --- a/src/components/views/typography/Caption.tsx +++ b/src/components/views/typography/Caption.tsx @@ -19,13 +19,14 @@ import React, { HTMLAttributes } from "react"; interface Props extends Omit, "className"> { children: React.ReactNode; + className?: string; isError?: boolean; } -export const Caption: React.FC = ({ children, isError, ...rest }) => { +export const Caption: React.FC = ({ children, className, isError, ...rest }) => { return ( { // 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; diff --git a/src/toasts/IncomingLegacyCallToast.tsx b/src/toasts/IncomingLegacyCallToast.tsx index 26ea393332..147a35a1d0 100644 --- a/src/toasts/IncomingLegacyCallToast.tsx +++ b/src/toasts/IncomingLegacyCallToast.tsx @@ -119,7 +119,7 @@ export default class IncomingLegacyCallToast extends React.Component {isVoice ? _t("Voice call") : _t("Video call")}
-
+
> = {}): RenderResult => { + const propsWithDefaults = { + acceptLabel: "Accept", + description:
Description
, + onAccept: () => {}, + onReject: () => {}, + rejectLabel: "Reject", + ...props, + }; + + return render(); +}; + +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(); + }); +}); diff --git a/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap new file mode 100644 index 0000000000..b6e47abff7 --- /dev/null +++ b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenericToast should render as expected with detail content 1`] = ` + +
+
+
+ Description +
+
+
+
+ Reject +
+
+ Accept +
+
+
+
+`; + +exports[`GenericToast should render as expected without detail content 1`] = ` + +
+
+
+ Description +
+ + Detail + +
+
+
+ Reject +
+
+ Accept +
+
+
+
+`; diff --git a/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap b/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap index 0aef239030..07cd6cc365 100644 --- a/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap +++ b/test/toasts/__snapshots__/UnverifiedSessionToast-test.tsx.snap @@ -27,8 +27,8 @@ exports[`UnverifiedSessionToast when rendering the toast should render as expect
-
ABC123 -
+