mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-21 00:28:08 +08:00
Implement the new invite modal designs
This commit is contained in:
parent
6865a13c3c
commit
8a14d60cb4
@ -87,6 +87,7 @@
|
||||
"@storybook/react": "^6.5.0-alpha.5",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/dom-screen-wake-lock": "^1.0.1",
|
||||
|
@ -14,96 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
background: rgba(3, 12, 27, 0.528);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogOverlay[data-state="open"] {
|
||||
animation: fade-in 200ms;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogOverlay[data-state="closed"] {
|
||||
animation: fade-out 130ms;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-sizing: border-box;
|
||||
inline-size: 520px;
|
||||
max-inline-size: 90%;
|
||||
max-block-size: 600px;
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog[data-state="open"] {
|
||||
animation: zoom-in 200ms;
|
||||
}
|
||||
|
||||
.dialog[data-state="closed"] {
|
||||
animation: zoom-out 130ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.dialog[data-state="open"] {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.dialog[data-state="closed"] {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -32,6 +32,7 @@ import classNames from "classnames";
|
||||
import { Heading } from "@vector-im/compound-web";
|
||||
|
||||
import styles from "./Modal.module.css";
|
||||
import overlayStyles from "./Overlay.module.css";
|
||||
import { useMediaQuery } from "./useMediaQuery";
|
||||
import { Glass } from "./Glass";
|
||||
|
||||
@ -85,9 +86,14 @@ export function Modal({
|
||||
dismissible={onDismiss !== undefined}
|
||||
>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay className={styles.overlay} />
|
||||
<Drawer.Overlay className={classNames(overlayStyles.bg)} />
|
||||
<Drawer.Content
|
||||
className={classNames(className, styles.modal, styles.drawer)}
|
||||
className={classNames(
|
||||
className,
|
||||
overlayStyles.overlay,
|
||||
styles.modal,
|
||||
styles.drawer
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
@ -108,12 +114,18 @@ export function Modal({
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(styles.overlay, styles.dialogOverlay)}
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent asChild {...rest}>
|
||||
<Glass
|
||||
frosted
|
||||
className={classNames(className, styles.modal, styles.dialog)}
|
||||
className={classNames(
|
||||
className,
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.modal,
|
||||
styles.dialog
|
||||
)}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
|
99
src/Overlay.module.css
Normal file
99
src/Overlay.module.css
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
.bg {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
background: rgba(3, 12, 27, 0.528);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bg.animate[data-state="open"] {
|
||||
animation: fade-in 200ms;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bg.animate[data-state="closed"] {
|
||||
animation: fade-out 130ms;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.overlay.animate {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay.animate[data-state="open"] {
|
||||
animation: zoom-in 200ms;
|
||||
}
|
||||
|
||||
.overlay.animate[data-state="closed"] {
|
||||
animation: zoom-out 130ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.overlay.animate[data-state="open"] {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.overlay.animate[data-state="closed"] {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
}
|
38
src/Toast.module.css
Normal file
38
src/Toast.module.css
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
.toast {
|
||||
color: var(--cpd-color-text-on-solid-primary);
|
||||
background: var(--cpd-color-alpha-gray-1200);
|
||||
padding-inline: var(--cpd-space-3x);
|
||||
padding-block: var(--cpd-space-1x);
|
||||
border: none;
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
box-shadow: var(--small-drop-shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-1x);
|
||||
}
|
||||
|
||||
.toast > h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toast > svg {
|
||||
color: var(--cpd-color-icon-on-solid-primary);
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: calc(-1 * var(--cpd-space-1x));
|
||||
}
|
108
src/Toast.tsx
Normal file
108
src/Toast.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
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 {
|
||||
ComponentType,
|
||||
FC,
|
||||
SVGAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import {
|
||||
Root as DialogRoot,
|
||||
Portal as DialogPortal,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Close as DialogClose,
|
||||
Title as DialogTitle,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import classNames from "classnames";
|
||||
import { Text } from "@vector-im/compound-web";
|
||||
|
||||
import styles from "./Toast.module.css";
|
||||
import overlayStyles from "./Overlay.module.css";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The controlled open state of the toast.
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* Callback for when the user dismisses the toast.
|
||||
*/
|
||||
onDismiss: () => void;
|
||||
/**
|
||||
* A number of milliseconds after which the toast should be automatically
|
||||
* dismissed.
|
||||
*/
|
||||
autoDismiss?: number;
|
||||
children: string;
|
||||
/**
|
||||
* A supporting icon to display within the toast.
|
||||
*/
|
||||
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A temporary message shown in an overlay in the center of the screen.
|
||||
*/
|
||||
export const Toast: FC<Props> = ({
|
||||
open,
|
||||
onDismiss,
|
||||
autoDismiss,
|
||||
children,
|
||||
Icon,
|
||||
}) => {
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) onDismiss();
|
||||
},
|
||||
[onDismiss]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && autoDismiss !== undefined) {
|
||||
const timeout = setTimeout(onDismiss, autoDismiss);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [open, autoDismiss, onDismiss]);
|
||||
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent asChild>
|
||||
<DialogClose
|
||||
className={classNames(
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.toast
|
||||
)}
|
||||
>
|
||||
<DialogTitle asChild>
|
||||
<Text as="h3" size="sm" weight="semibold">
|
||||
{children}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
);
|
||||
};
|
@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.copyButton {
|
||||
.url {
|
||||
text-align: center;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
margin-block-end: var(--cpd-space-8x);
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -14,15 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Room } from "matrix-js-sdk";
|
||||
import { Button, Text } from "@vector-im/compound-web";
|
||||
import { ReactComponent as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
||||
import { ReactComponent as CheckIcon } from "@vector-im/compound-design-tokens/icons/check.svg";
|
||||
import useClipboard from "react-use-clipboard";
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { Toast } from "../Toast";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
@ -33,19 +37,48 @@ interface Props {
|
||||
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
||||
const url = useMemo(
|
||||
() =>
|
||||
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
||||
[room, roomSharedKey]
|
||||
);
|
||||
const [, setCopied] = useClipboard(url);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
const onToastDismiss = useCallback(() => setToastOpen(false), [setToastOpen]);
|
||||
|
||||
const onButtonClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setCopied();
|
||||
onDismiss();
|
||||
setToastOpen(true);
|
||||
},
|
||||
[setCopied, onDismiss]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={t("Invite to this call")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getAbsoluteRoomUrl(
|
||||
room.roomId,
|
||||
room.name,
|
||||
roomSharedKey ?? undefined
|
||||
)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</Modal>
|
||||
<>
|
||||
<Modal title={t("Invite to this call")} open={open} onDismiss={onDismiss}>
|
||||
<Text className={styles.url} size="sm" weight="semibold">
|
||||
{url}
|
||||
</Text>
|
||||
<Button
|
||||
className={styles.button}
|
||||
Icon={LinkIcon}
|
||||
onClick={onButtonClick}
|
||||
data-testid="modal_inviteLink"
|
||||
>
|
||||
{t("Copy link")}
|
||||
</Button>
|
||||
</Modal>
|
||||
<Toast
|
||||
open={toastOpen}
|
||||
onDismiss={onToastDismiss}
|
||||
autoDismiss={2000}
|
||||
Icon={CheckIcon}
|
||||
>
|
||||
{t("Link copied to clipboard")}
|
||||
</Toast>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
59
test/Toast-test.tsx
Normal file
59
test/Toast-test.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
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 { screen, render } from "@testing-library/react";
|
||||
import { Toast } from "../src/Toast";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { withFakeTimers } from "./utils";
|
||||
|
||||
test("Toast renders", () => {
|
||||
render(
|
||||
<Toast open={false} onDismiss={() => {}}>
|
||||
Hello world!
|
||||
</Toast>
|
||||
);
|
||||
expect(screen.queryByRole("dialog")).toBe(null);
|
||||
render(
|
||||
<Toast open={true} onDismiss={() => {}}>
|
||||
Hello world!
|
||||
</Toast>
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("Toast dismisses when clicked", async () => {
|
||||
const onDismiss = jest.fn();
|
||||
render(
|
||||
<Toast open={true} onDismiss={onDismiss}>
|
||||
Hello world!
|
||||
</Toast>
|
||||
);
|
||||
await userEvent.click(screen.getByRole("dialog"));
|
||||
expect(onDismiss).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("Toast dismisses itself after the specified timeout", async () => {
|
||||
withFakeTimers(() => {
|
||||
const onDismiss = jest.fn();
|
||||
render(
|
||||
<Toast open={true} onDismiss={onDismiss} autoDismiss={2000}>
|
||||
Hello world!
|
||||
</Toast>
|
||||
);
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(onDismiss).toHaveBeenCalled();
|
||||
});
|
||||
});
|
22
test/__snapshots__/Toast-test.tsx.snap
Normal file
22
test/__snapshots__/Toast-test.tsx.snap
Normal file
@ -0,0 +1,22 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Toast renders 1`] = `
|
||||
<button
|
||||
aria-describedby="radix-:r5:"
|
||||
aria-labelledby="radix-:r4:"
|
||||
class="overlay animate toast"
|
||||
data-state="open"
|
||||
id="radix-:r3:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<h3
|
||||
class="_font-body-sm-semibold_1jx6b_45"
|
||||
id="radix-:r4:"
|
||||
>
|
||||
Hello world!
|
||||
</h3>
|
||||
</button>
|
||||
`;
|
@ -18,15 +18,7 @@ import { Mocked, mocked } from "jest-mock";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
||||
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
||||
|
||||
const withFakeTimers = (continuation: () => void) => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
continuation();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
};
|
||||
import { withFakeTimers } from "../utils";
|
||||
|
||||
const withMockedPosthog = (
|
||||
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
||||
|
24
test/utils.ts
Normal file
24
test/utils.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
export function withFakeTimers(continuation: () => void): void {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
continuation();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
}
|
@ -4394,6 +4394,11 @@
|
||||
"@testing-library/dom" "^9.0.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@testing-library/user-event@^14.5.1":
|
||||
version "14.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.1.tgz#27337d72046d5236b32fd977edee3f74c71d332f"
|
||||
integrity sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
|
Loading…
Reference in New Issue
Block a user