Refactor buttons
This commit is contained in:
parent
db1fb064ca
commit
4cb144809f
19 changed files with 313 additions and 622 deletions
|
|
@ -1,37 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
import useClipboard from "react-use-clipboard";
|
|
||||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
|
||||||
import { ReactComponent as CopyIcon } from "./icons/Copy.svg";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./CopyButton.module.css";
|
|
||||||
|
|
||||||
export function CopyButton({ value, className, children, ...rest }) {
|
|
||||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
|
||||||
const { buttonProps } = useButton({
|
|
||||||
onPress: () => setCopied(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
{...buttonProps}
|
|
||||||
className={classNames(
|
|
||||||
styles.copyButton,
|
|
||||||
{ [styles.copied]: isCopied },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<>
|
|
||||||
<span>Copied!</span>
|
|
||||||
<CheckIcon />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>{children || value}</span>
|
|
||||||
<CopyIcon />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
.copyButton {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #0dbd8b;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #0dbd8b;
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
transition: border-color 250ms, background-color 250ms;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton span {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 15px;
|
|
||||||
margin-right: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton:not(.copied) svg * {
|
|
||||||
fill: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton.copied {
|
|
||||||
border-color: transparent;
|
|
||||||
background-color: #0dbd8b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton.copied svg * {
|
|
||||||
stroke: white;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback } from "react";
|
import React from "react";
|
||||||
import { ButtonTooltip, HeaderButton } from "./RoomButton";
|
import { ButtonTooltip, Button } from "./button";
|
||||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||||
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
||||||
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
||||||
|
|
@ -11,10 +11,10 @@ import { Item } from "@react-stately/collections";
|
||||||
export function GridLayoutMenu({ layout, setLayout }) {
|
export function GridLayoutMenu({ layout, setLayout }) {
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<HeaderButton>
|
<Button variant="icon">
|
||||||
<ButtonTooltip>Layout Type</ButtonTooltip>
|
<ButtonTooltip>Layout Type</ButtonTooltip>
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||||
</HeaderButton>
|
</Button>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||||
<Item key="freedom" textValue="Freedom">
|
<Item key="freedom" textValue="Freedom">
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
import { ReactComponent as LogoIcon } from "./Logo.svg";
|
import { ReactComponent as LogoIcon } from "./Logo.svg";
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||||
import { HeaderDropdownItem, UserMenu } from "./RoomButton";
|
import { useButton } from "@react-aria/button";
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }) {
|
export function Header({ children, className, ...rest }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -56,25 +56,13 @@ export function RoomHeaderInfo({ roomName }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomSetupHeaderInfo({ onBack, roomName }) {
|
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
|
||||||
|
const ref = useRef();
|
||||||
|
const { buttonProps } = useButton(rest, ref);
|
||||||
return (
|
return (
|
||||||
<button className={styles.backButton} onClick={onBack}>
|
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||||
<ArrowLeftIcon width={16} height={16} />
|
<ArrowLeftIcon width={16} height={16} />
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
<RoomHeaderInfo roomName={roomName} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserDropdownMenu({ userName, signedIn, onLogout }) {
|
|
||||||
if (!signedIn) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserMenu userName={userName}>
|
|
||||||
<HeaderDropdownItem onClick={onLogout} className={styles.signOutButton}>
|
|
||||||
Sign Out
|
|
||||||
</HeaderDropdownItem>
|
|
||||||
</UserMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
import ColorHash from "color-hash";
|
import ColorHash from "color-hash";
|
||||||
import styles from "./Home.module.css";
|
import styles from "./Home.module.css";
|
||||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||||
import { Center, Content, Modal } from "./Layout";
|
import { Center, Content, Modal } from "./Layout";
|
||||||
import {
|
import {
|
||||||
GroupCallIntent,
|
GroupCallIntent,
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from "matrix-js-sdk/src/browser-index";
|
} from "matrix-js-sdk/src/browser-index";
|
||||||
import { Facepile } from "./Facepile";
|
import { Facepile } from "./Facepile";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
const colorHash = new ColorHash({ lightness: 0.3 });
|
const colorHash = new ColorHash({ lightness: 0.3 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,18 +44,6 @@ export const InputField = forwardRef(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Button = forwardRef(({ className, children, ...rest }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(styles.button, className)}
|
|
||||||
ref={ref}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ErrorMessage({ children }) {
|
export function ErrorMessage({ children }) {
|
||||||
return <p className={styles.errorMessage}>{children}</p>;
|
return <p className={styles.errorMessage}>{children}</p>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,29 +151,6 @@
|
||||||
border: 1.5px solid var(--inputBorderColorFocused) !important;
|
border: 1.5px solid var(--inputBorderColorFocused) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
vertical-align: middle;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: var(--primaryColor);
|
|
||||||
width: auto;
|
|
||||||
padding: 7px 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:active {
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Modal, ModalContent } from "./Modal";
|
import { Modal, ModalContent } from "./Modal";
|
||||||
import { CopyButton } from "./CopyButton";
|
import { CopyButton } from "./button";
|
||||||
|
|
||||||
export function InviteModal({ roomUrl, ...rest }) {
|
export function InviteModal({ roomUrl, ...rest }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
import { Header, HeaderLogo, LeftNav } from "./Header";
|
import { Header, HeaderLogo, LeftNav } from "./Header";
|
||||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||||
import { Center, Content, Info, Modal } from "./Layout";
|
import { Center, Content, Info, Modal } from "./Layout";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
export function LoginPage({ onLogin }) {
|
export function LoginPage({ onLogin }) {
|
||||||
const [homeserver, setHomeServer] = useState(
|
const [homeserver, setHomeServer] = useState(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { ButtonTooltip, RoomButton } from "./RoomButton";
|
import { ButtonTooltip, Button } from "./button";
|
||||||
import { Menu } from "./Menu";
|
import { Menu } from "./Menu";
|
||||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
|
@ -37,10 +37,10 @@ export function OverflowMenu({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoverMenuTrigger>
|
<PopoverMenuTrigger>
|
||||||
<RoomButton>
|
<Button variant="toolbar">
|
||||||
<ButtonTooltip>More</ButtonTooltip>
|
<ButtonTooltip>More</ButtonTooltip>
|
||||||
<OverflowIcon />
|
<OverflowIcon />
|
||||||
</RoomButton>
|
</Button>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="More menu" onAction={onAction}>
|
<Menu {...props} label="More menu" onAction={onAction}>
|
||||||
<Item key="invite" textValue="Invite people">
|
<Item key="invite" textValue="Invite people">
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
import { Header, LeftNav, HeaderLogo } from "./Header";
|
import { Header, LeftNav, HeaderLogo } from "./Header";
|
||||||
import { FieldRow, InputField, Button, ErrorMessage } from "./Input";
|
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||||
import { Center, Content, Info, Modal } from "./Layout";
|
import { Center, Content, Info, Modal } from "./Layout";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
export function RegisterPage({ onRegister }) {
|
export function RegisterPage({ onRegister }) {
|
||||||
const registerUsernameRef = useRef();
|
const registerUsernameRef = useRef();
|
||||||
|
|
|
||||||
10
src/Room.jsx
10
src/Room.jsx
|
|
@ -18,11 +18,13 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import styles from "./Room.module.css";
|
import styles from "./Room.module.css";
|
||||||
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
|
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
CopyButton,
|
||||||
HangupButton,
|
HangupButton,
|
||||||
MicButton,
|
MicButton,
|
||||||
VideoButton,
|
VideoButton,
|
||||||
ScreenshareButton,
|
ScreenshareButton,
|
||||||
} from "./RoomButton";
|
} from "./button";
|
||||||
import {
|
import {
|
||||||
Header,
|
Header,
|
||||||
LeftNav,
|
LeftNav,
|
||||||
|
|
@ -30,7 +32,6 @@ import {
|
||||||
RoomHeaderInfo,
|
RoomHeaderInfo,
|
||||||
RoomSetupHeaderInfo,
|
RoomSetupHeaderInfo,
|
||||||
} from "./Header";
|
} from "./Header";
|
||||||
import { Button } from "./Input";
|
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import VideoGrid, {
|
import VideoGrid, {
|
||||||
useVideoGridLayout,
|
useVideoGridLayout,
|
||||||
|
|
@ -47,7 +48,6 @@ import * as Sentry from "@sentry/react";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
import { CopyButton } from "./CopyButton";
|
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
|
|
@ -276,7 +276,7 @@ function RoomSetupView({
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomSetupHeaderInfo
|
<RoomSetupHeaderInfo
|
||||||
onBack={() => history.goBack()}
|
onPress={() => history.goBack()}
|
||||||
roomName={roomName}
|
roomName={roomName}
|
||||||
/>
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
|
|
@ -310,7 +310,7 @@ function RoomSetupView({
|
||||||
<Button
|
<Button
|
||||||
className={styles.joinCallButton}
|
className={styles.joinCallButton}
|
||||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
||||||
onClick={onEnter}
|
onPress={onEnter}
|
||||||
>
|
>
|
||||||
Join call now
|
Join call now
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
import React, { useRef, useState, useEffect, forwardRef } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./RoomButton.module.css";
|
|
||||||
import { ReactComponent as MicIcon } from "./icons/Mic.svg";
|
|
||||||
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
|
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
|
||||||
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
|
|
||||||
import { ReactComponent as HangupIcon } from "./icons/Hangup.svg";
|
|
||||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
|
||||||
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
|
||||||
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
|
||||||
import { ReactComponent as ScreenshareIcon } from "./icons/Screenshare.svg";
|
|
||||||
import { ReactComponent as ChevronIcon } from "./icons/Chevron.svg";
|
|
||||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
|
||||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
|
|
||||||
export const RoomButton = forwardRef(
|
|
||||||
({ on, off, className, children, ...rest }, ref) => {
|
|
||||||
const { buttonProps } = useButton(rest, ref);
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(styles.roomButton, className, {
|
|
||||||
[styles.on]: on,
|
|
||||||
[styles.off]: off,
|
|
||||||
})}
|
|
||||||
{...buttonProps}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function DropdownButton({ onChange, options, value, children }) {
|
|
||||||
const buttonRef = useRef();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onClick() {
|
|
||||||
if (open) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("click", onClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("click", onClick);
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.dropdownButtonContainer}>
|
|
||||||
{children}
|
|
||||||
<button
|
|
||||||
ref={buttonRef}
|
|
||||||
className={styles.dropdownButton}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
<ChevronIcon />
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<div className={styles.dropdownContainer}>
|
|
||||||
<ul>
|
|
||||||
{options.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.value}
|
|
||||||
className={classNames({
|
|
||||||
[styles.dropdownActiveItem]: item.value === value,
|
|
||||||
})}
|
|
||||||
onClick={() => onChange(item)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MicButton({ muted, ...rest }) {
|
|
||||||
return (
|
|
||||||
<RoomButton {...rest} off={muted}>
|
|
||||||
<ButtonTooltip>
|
|
||||||
{muted ? "Unmute microphone" : "Mute microphone"}
|
|
||||||
</ButtonTooltip>
|
|
||||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
|
||||||
</RoomButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoButton({ muted, ...rest }) {
|
|
||||||
return (
|
|
||||||
<RoomButton {...rest} off={muted}>
|
|
||||||
<ButtonTooltip>
|
|
||||||
{muted ? "Turn on camera" : "Turn off camera"}
|
|
||||||
</ButtonTooltip>
|
|
||||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
|
||||||
</RoomButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<RoomButton {...rest} on={enabled}>
|
|
||||||
<ButtonTooltip>
|
|
||||||
{enabled ? "Stop sharing screen" : "Share screen"}
|
|
||||||
</ButtonTooltip>
|
|
||||||
<ScreenshareIcon />
|
|
||||||
</RoomButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HangupButton({ className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<RoomButton
|
|
||||||
className={classNames(styles.hangupButton, className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<ButtonTooltip>Leave</ButtonTooltip>
|
|
||||||
<HangupIcon />
|
|
||||||
</RoomButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeaderButton = forwardRef(
|
|
||||||
({ on, className, children, ...rest }, ref) => {
|
|
||||||
const { buttonProps } = useButton(rest, ref);
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(styles.headerButton, className, {
|
|
||||||
[styles.on]: on,
|
|
||||||
})}
|
|
||||||
{...buttonProps}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function HeaderDropdownButton({ children, content }) {
|
|
||||||
const buttonRef = useRef();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onClick() {
|
|
||||||
if (open) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("click", onClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("click", onClick);
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.dropdownButtonContainer}>
|
|
||||||
<button
|
|
||||||
ref={buttonRef}
|
|
||||||
className={classNames(styles.headerButton, { [styles.on]: open })}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.dropdownContainer,
|
|
||||||
styles.headerDropdownContainer
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ul>{content}</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeaderDropdownItem({ active, children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={classNames(className, {
|
|
||||||
[styles.dropdownActiveItem]: active,
|
|
||||||
})}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserMenu({ userName, children }) {
|
|
||||||
return (
|
|
||||||
<HeaderDropdownButton content={children}>
|
|
||||||
<ButtonTooltip>Profile</ButtonTooltip>
|
|
||||||
<div className={styles.userButton}>
|
|
||||||
<UserIcon />
|
|
||||||
<span>{userName}</span>
|
|
||||||
</div>
|
|
||||||
</HeaderDropdownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsButton(props) {
|
|
||||||
return (
|
|
||||||
<HeaderButton {...props}>
|
|
||||||
<ButtonTooltip>Show Dev Tools</ButtonTooltip>
|
|
||||||
<SettingsIcon width={20} height={20} />
|
|
||||||
</HeaderButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LayoutToggleButton({ layout, setLayout, ...rest }) {
|
|
||||||
return (
|
|
||||||
<HeaderDropdownButton
|
|
||||||
{...rest}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<HeaderDropdownItem onClick={() => setLayout("freedom")}>
|
|
||||||
<FreedomIcon />
|
|
||||||
<span>Freedom</span>
|
|
||||||
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
|
|
||||||
</HeaderDropdownItem>
|
|
||||||
<HeaderDropdownItem onClick={() => setLayout("spotlight")}>
|
|
||||||
<SpotlightIcon />
|
|
||||||
<span>Spotlight</span>
|
|
||||||
{layout === "spotlight" && (
|
|
||||||
<CheckIcon className={styles.checkIcon} />
|
|
||||||
)}
|
|
||||||
</HeaderDropdownItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ButtonTooltip>Layout Type</ButtonTooltip>
|
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
|
||||||
</HeaderDropdownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ButtonTooltip({ className, children }) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.buttonTooltip, className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2021 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.roomButton,
|
|
||||||
.headerButton,
|
|
||||||
.dropdownButton {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50px;
|
|
||||||
background-color: #394049;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton:hover {
|
|
||||||
background-color: #8d97a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton:active {
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton.on,
|
|
||||||
.roomButton.off {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton svg * {
|
|
||||||
fill: #8e99a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton:hover svg * {
|
|
||||||
fill: #8d97a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton:hover svg * {
|
|
||||||
fill: #8d97a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton.on svg * {
|
|
||||||
fill: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hangupButton,
|
|
||||||
.hangupButton:hover {
|
|
||||||
background-color: #ff5b55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton.on svg * {
|
|
||||||
fill: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton.off svg * {
|
|
||||||
fill: #21262c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownButtonContainer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownButton {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: #394049;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownButton:hover {
|
|
||||||
background-color: #8d97a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownButton:hover svg * {
|
|
||||||
fill: #8d97a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(0, -100%);
|
|
||||||
top: -5px;
|
|
||||||
background-color: var(--bgColor4);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerDropdownContainer {
|
|
||||||
transform: translate(-100%, 36px);
|
|
||||||
left: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkIcon {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkIcon * {
|
|
||||||
stroke: var(--textColor1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer li {
|
|
||||||
position: relative;
|
|
||||||
padding: 12px;
|
|
||||||
width: 200px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer li > * {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer li > :last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownContainer li:hover {
|
|
||||||
background-color: var(--bgColor5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownActiveItem {
|
|
||||||
color: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonTooltip {
|
|
||||||
display: none;
|
|
||||||
background-color: var(--bgColor2);
|
|
||||||
position: absolute;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 10px;
|
|
||||||
color: var(--textColor1);
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 135px;
|
|
||||||
width: max-content;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonTooltip.bottomRight {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomButton:hover .buttonTooltip {
|
|
||||||
display: flex;
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton:hover .buttonTooltip {
|
|
||||||
display: flex;
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--textColor1);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userButton > * {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userButton svg * {
|
|
||||||
fill: var(--textColor1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton:hover .userButton > * {
|
|
||||||
color: var(--textColor1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton:hover .userButton svg * {
|
|
||||||
fill: var(--textColor1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userButton > :last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { ButtonTooltip, HeaderButton } from "./RoomButton";
|
import { ButtonTooltip, Button } from "./button";
|
||||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||||
|
|
@ -48,13 +48,13 @@ export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
|
||||||
}, [signedIn, userName]);
|
}, [signedIn, userName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger onAction={onAction} placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<HeaderButton className={styles.userButton}>
|
<Button variant="icon" className={styles.userButton}>
|
||||||
<ButtonTooltip>Profile</ButtonTooltip>
|
<ButtonTooltip>Profile</ButtonTooltip>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</HeaderButton>
|
</Button>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="User menu">
|
<Menu {...props} label="User menu" onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label }) => (
|
{items.map(({ key, icon: Icon, label }) => (
|
||||||
<Item key={key} textValue={label}>
|
<Item key={key} textValue={label}>
|
||||||
<Icon />
|
<Icon />
|
||||||
|
|
|
||||||
101
src/button/Button.jsx
Normal file
101
src/button/Button.jsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import styles from "./Button.module.css";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||||
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
|
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||||
|
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||||
|
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||||
|
import { useButton } from "@react-aria/button";
|
||||||
|
import { useObjectRef } from "@react-aria/utils";
|
||||||
|
|
||||||
|
const variantToClassName = {
|
||||||
|
default: [styles.button],
|
||||||
|
toolbar: [styles.toolbarButton],
|
||||||
|
icon: [styles.iconButton],
|
||||||
|
copy: [styles.copyButton],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Button = forwardRef(
|
||||||
|
({ variant = "default", on, off, className, children, ...rest }, ref) => {
|
||||||
|
const buttonRef = useObjectRef(ref);
|
||||||
|
const { buttonProps } = useButton(rest, buttonRef);
|
||||||
|
|
||||||
|
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||||
|
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||||
|
let filteredButtonProps = buttonProps;
|
||||||
|
|
||||||
|
if (rest.type === "submit" && !rest.onPress) {
|
||||||
|
const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
|
||||||
|
filteredButtonProps = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames(variantToClassName[variant], className, {
|
||||||
|
[styles.on]: on,
|
||||||
|
[styles.off]: off,
|
||||||
|
})}
|
||||||
|
{...filteredButtonProps}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function ButtonTooltip({ className, children }) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.buttonTooltip, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MicButton({ muted, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
|
<ButtonTooltip>
|
||||||
|
{muted ? "Unmute microphone" : "Mute microphone"}
|
||||||
|
</ButtonTooltip>
|
||||||
|
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoButton({ muted, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
|
<ButtonTooltip>
|
||||||
|
{muted ? "Turn on camera" : "Turn off camera"}
|
||||||
|
</ButtonTooltip>
|
||||||
|
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Button variant="toolbar" {...rest} on={enabled}>
|
||||||
|
<ButtonTooltip>
|
||||||
|
{enabled ? "Stop sharing screen" : "Share screen"}
|
||||||
|
</ButtonTooltip>
|
||||||
|
<ScreenshareIcon />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HangupButton({ className, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="toolbar"
|
||||||
|
className={classNames(styles.hangupButton, className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<ButtonTooltip>Leave</ButtonTooltip>
|
||||||
|
<HangupIcon />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/button/Button.module.css
Normal file
155
src/button/Button.module.css
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.toolbarButton,
|
||||||
|
.iconButton {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--primaryColor);
|
||||||
|
padding: 7px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background-color: #394049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton:hover {
|
||||||
|
background-color: #8d97a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton.on,
|
||||||
|
.toolbarButton.off {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton svg * {
|
||||||
|
fill: #8e99a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton:hover svg * {
|
||||||
|
fill: #8d97a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton:hover svg * {
|
||||||
|
fill: #8d97a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton.on svg * {
|
||||||
|
fill: #0dbd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hangupButton,
|
||||||
|
.hangupButton:hover {
|
||||||
|
background-color: #ff5b55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton.on svg * {
|
||||||
|
fill: #0dbd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton.off svg * {
|
||||||
|
fill: #21262c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonTooltip {
|
||||||
|
display: none;
|
||||||
|
background-color: var(--bgColor2);
|
||||||
|
position: absolute;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--textColor1);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 135px;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonTooltip.bottomRight {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButton:hover .buttonTooltip {
|
||||||
|
display: flex;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton:hover .buttonTooltip {
|
||||||
|
display: flex;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #0dbd8b;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #0dbd8b;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
transition: border-color 250ms, background-color 250ms;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-right: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton:not(.on) svg * {
|
||||||
|
fill: #0dbd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton.on {
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: #0dbd8b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton.on svg * {
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
25
src/button/CopyButton.jsx
Normal file
25
src/button/CopyButton.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import useClipboard from "react-use-clipboard";
|
||||||
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
export function CopyButton({ value, children, ...rest }) {
|
||||||
|
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button {...rest} variant="copy" on={isCopied} onPress={setCopied}>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
<span>Copied!</span>
|
||||||
|
<CheckIcon />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{children || value}</span>
|
||||||
|
<CopyIcon />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
src/button/index.js
Normal file
2
src/button/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./Button";
|
||||||
|
export * from "./CopyButton";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue