mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Port modal / popover context.
This commit is contained in:
@@ -10,7 +10,7 @@ interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChann
|
||||
|
||||
const fallback = '/assets/group.png';
|
||||
export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
|
||||
const { client } = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const { size, target, attachment, isServerChannel: server, animate, children, as, ...imgProps } = props;
|
||||
const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
|
||||
|
||||
@@ -20,7 +20,7 @@ const ServerText = styled.div`
|
||||
|
||||
const fallback = '/assets/group.png';
|
||||
export default function ServerIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
|
||||
const { client } = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const { target, attachment, size, animate, server_name, children, as, ...imgProps } = props;
|
||||
const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
|
||||
|
||||
26
src/components/common/Tooltip.tsx
Normal file
26
src/components/common/Tooltip.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import styled from "styled-components";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { Position, Tooltip as TooltipCore, TooltipProps } from "react-tippy";
|
||||
|
||||
type Props = Omit<TooltipProps, 'html'> & {
|
||||
position?: Position;
|
||||
children: Children;
|
||||
content: Children;
|
||||
}
|
||||
|
||||
const TooltipBase = styled.div`
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
color: var(--foreground);
|
||||
background: var(--secondary-background);
|
||||
`;
|
||||
|
||||
export default function Tooltip(props: Props) {
|
||||
return (
|
||||
<TooltipCore
|
||||
{...props}
|
||||
// @ts-expect-error
|
||||
html={<TooltipBase>{props.content}</TooltipBase>} />
|
||||
);
|
||||
}
|
||||
14
src/components/common/UserCheckbox.tsx
Normal file
14
src/components/common/UserCheckbox.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { User } from "revolt.js";
|
||||
import UserIcon from "./UserIcon";
|
||||
import Checkbox, { CheckboxProps } from "../ui/Checkbox";
|
||||
|
||||
type UserProps = Omit<CheckboxProps, "children"> & { user: User };
|
||||
|
||||
export default function UserCheckbox({ user, ...props }: UserProps) {
|
||||
return (
|
||||
<Checkbox {...props}>
|
||||
<UserIcon target={user} size={32} />
|
||||
{user.username}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
@@ -49,11 +49,11 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||
|
||||
const fallback = '/assets/user.png';
|
||||
export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
|
||||
const { client } = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const { target, attachment, size, voice, status, animate, children, as, ...svgProps } = props;
|
||||
const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate)
|
||||
?? client.users.getDefaultAvatarURL(target!._id);
|
||||
?? (target && client.users.getDefaultAvatarURL(target._id));
|
||||
|
||||
return (
|
||||
<IconBase {...svgProps}
|
||||
|
||||
41
src/components/markdown/Emoji.tsx
Normal file
41
src/components/markdown/Emoji.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
var EMOJI_PACK = 'mutant';
|
||||
const REVISION = 3;
|
||||
|
||||
/*export function setEmojiPack(pack: EmojiPacks) {
|
||||
EMOJI_PACK = pack;
|
||||
}*/
|
||||
|
||||
// Taken from Twemoji source code.
|
||||
// scripts/build.js#344
|
||||
// grabTheRightIcon(rawText);
|
||||
const UFE0Fg = /\uFE0F/g;
|
||||
const U200D = String.fromCharCode(0x200D);
|
||||
function toCodePoint(emoji: string) {
|
||||
return twemoji.convert.toCodePoint(emoji.indexOf(U200D) < 0 ?
|
||||
emoji.replace(UFE0Fg, '') :
|
||||
emoji
|
||||
);
|
||||
}
|
||||
|
||||
function parseEmoji(emoji: string) {
|
||||
let codepoint = toCodePoint(emoji);
|
||||
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
|
||||
}
|
||||
|
||||
export function Emoji({ emoji, size }: { emoji: string, size?: number }) {
|
||||
return (
|
||||
<img
|
||||
alt={emoji}
|
||||
className="emoji"
|
||||
draggable={false}
|
||||
src={parseEmoji(emoji)}
|
||||
style={size ? { width: `${size}px`, height: `${size}px` } : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function generateEmoji(emoji: string) {
|
||||
return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(emoji)}" />`;
|
||||
}
|
||||
202
src/components/markdown/Markdown.module.scss
Normal file
202
src/components/markdown/Markdown.module.scss
Normal file
@@ -0,0 +1,202 @@
|
||||
@import "@fontsource/fira-mono/400.css";
|
||||
|
||||
.markdown {
|
||||
:global(.emoji) {
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
margin: 0 0.05em 0 0.1em;
|
||||
vertical-align: -0.2em;
|
||||
}
|
||||
|
||||
&[data-large-emojis="true"] :global(.emoji) {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 1px;
|
||||
margin-right: 2px;
|
||||
vertical-align: -.3em;
|
||||
}
|
||||
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&[data-type="mention"] {
|
||||
padding: 0 6px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
background: var(--secondary-background);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style-position: inside;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 2px 0;
|
||||
padding: 2px 0;
|
||||
border-radius: 4px;
|
||||
background: var(--hover);
|
||||
border-inline-start: 4px solid var(--tertiary-background);
|
||||
|
||||
> * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
overflow-x: scroll;
|
||||
border-radius: 3px;
|
||||
background: var(--block) !important;
|
||||
}
|
||||
|
||||
p > code {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: white;
|
||||
font-size: 90%;
|
||||
border-radius: 4px;
|
||||
background: var(--block);
|
||||
font-family: "Fira Mono", monospace;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 6px;
|
||||
border: 1px solid var(--tertiary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.katex-block) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:global(.spoiler) {
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: transparent;
|
||||
border-radius: 4px;
|
||||
background: #151515;
|
||||
|
||||
&:global(.shown) {
|
||||
cursor: auto;
|
||||
user-select: all;
|
||||
color: var(--foreground);
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.code) {
|
||||
font-family: "Fira Mono", monospace;
|
||||
|
||||
:global(.lang) {
|
||||
// height: 8px;
|
||||
// position: relative;
|
||||
|
||||
div {
|
||||
// margin-left: -5px;
|
||||
// margin-top: -16px;
|
||||
// position: absolute;
|
||||
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
background: var(--accent);
|
||||
|
||||
font-size: 10px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 2px #787676;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px #787676;
|
||||
}
|
||||
}
|
||||
|
||||
// ! FIXME: had to change this temporarily due to overflow
|
||||
width: fit-content;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label:before {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
content: 'a';
|
||||
font-size: 10px;
|
||||
margin-right: 6px;
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="checkbox"][checked="true"] + label:before {
|
||||
content: '✓';
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label {
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
17
src/components/markdown/Markdown.tsx
Normal file
17
src/components/markdown/Markdown.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Suspense, lazy } from "preact/compat";
|
||||
|
||||
const Renderer = lazy(() => import('./Renderer'));
|
||||
|
||||
export interface MarkdownProps {
|
||||
content?: string;
|
||||
disallowBigEmoji?: boolean;
|
||||
}
|
||||
|
||||
export default function Markdown(props: MarkdownProps) {
|
||||
return (
|
||||
// @ts-expect-error
|
||||
<Suspense fallback="Getting ready to render Markdown...">
|
||||
<Renderer {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
170
src/components/markdown/Renderer.tsx
Normal file
170
src/components/markdown/Renderer.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { RE_MENTIONS } from "revolt.js";
|
||||
import { generateEmoji } from "./Emoji";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { MarkdownProps } from "./Markdown";
|
||||
import styles from "./Markdown.module.scss";
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Prism from "prismjs";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
|
||||
import MarkdownKatex from "@traptitech/markdown-it-katex";
|
||||
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
|
||||
|
||||
// @ts-ignore
|
||||
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
|
||||
// @ts-ignore
|
||||
import MarkdownSup from "markdown-it-sup";
|
||||
// @ts-ignore
|
||||
import MarkdownSub from "markdown-it-sub";
|
||||
|
||||
// Handler for code block copy.
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).copycode = function(element: HTMLDivElement) {
|
||||
try {
|
||||
let code = element.parentElement?.parentElement?.children[1];
|
||||
if (code) {
|
||||
navigator.clipboard.writeText((code as any).innerText.trim());
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
}
|
||||
|
||||
export const md: MarkdownIt = MarkdownIt({
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
highlight: (str, lang) => {
|
||||
let v = Prism.languages[lang];
|
||||
if (v) {
|
||||
let out = Prism.highlight(str, v, lang);
|
||||
return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
|
||||
}
|
||||
|
||||
return `<pre class="code"><code>${md.utils.escapeHtml(str)}</code></pre>`;
|
||||
}
|
||||
})
|
||||
.disable("image")
|
||||
.use(MarkdownEmoji/*, { defs: emojiDictionary }*/)
|
||||
.use(MarkdownSpoilers)
|
||||
.use(MarkdownSup)
|
||||
.use(MarkdownSub)
|
||||
.use(MarkdownKatex, {
|
||||
throwOnError: false,
|
||||
maxExpand: 0
|
||||
});
|
||||
|
||||
// ? Force links to open _blank.
|
||||
// From: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
|
||||
const defaultRender =
|
||||
md.renderer.rules.link_open ||
|
||||
function(tokens, idx, options, _env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Handler for internal links, pushes events to React using magic.
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).internalHandleURL = function(element: HTMLAnchorElement) {
|
||||
const url = new URL(element.href, location as any);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname.startsWith("/@")) {
|
||||
//InternalEventEmitter.emit("openProfile", pathname.substr(2));
|
||||
} else {
|
||||
//InternalEventEmitter.emit("navigate", pathname);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
|
||||
let internal;
|
||||
const hIndex = tokens[idx].attrIndex("href");
|
||||
if (hIndex >= 0) {
|
||||
try {
|
||||
// For internal links, we should use our own handler to use react-router history.
|
||||
// @ts-ignore
|
||||
const href = tokens[idx].attrs[hIndex][1];
|
||||
const url = new URL(href, location as any);
|
||||
|
||||
if (url.hostname === location.hostname) {
|
||||
internal = true;
|
||||
// I'm sorry.
|
||||
tokens[idx].attrPush([
|
||||
"onclick",
|
||||
"internalHandleURL(this); return false"
|
||||
]);
|
||||
|
||||
if (url.pathname.startsWith("/@")) {
|
||||
tokens[idx].attrPush(["data-type", "mention"]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore the error, treat as normal link.
|
||||
}
|
||||
}
|
||||
|
||||
if (!internal) {
|
||||
// Add target=_blank for external links.
|
||||
const aIndex = tokens[idx].attrIndex("target");
|
||||
|
||||
if (aIndex < 0) {
|
||||
tokens[idx].attrPush(["target", "_blank"]);
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
tokens[idx].attrs[aIndex][1] = "_blank";
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
|
||||
md.renderer.rules.emoji = function(token, idx) {
|
||||
return generateEmoji(token[idx].content);
|
||||
};
|
||||
|
||||
const RE_TWEMOJI = /:(\w+):/g;
|
||||
|
||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||
const client = useContext(AppContext);
|
||||
if (typeof content === "undefined") return null;
|
||||
if (content.length === 0) return null;
|
||||
|
||||
// We replace the message with the mention at the time of render.
|
||||
// We don't care if the mention changes.
|
||||
let newContent = content.replace(
|
||||
RE_MENTIONS,
|
||||
(sub: string, ...args: any[]) => {
|
||||
const id = args[0],
|
||||
user = client.users.get(id);
|
||||
|
||||
if (user) {
|
||||
return `[@${user.username}](/@${id})`;
|
||||
}
|
||||
|
||||
return sub;
|
||||
}
|
||||
);
|
||||
|
||||
const useLargeEmojis = disallowBigEmoji ? false : content.replace(RE_TWEMOJI, '').trim().length === 0;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={styles.markdown}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: md.render(newContent)
|
||||
}}
|
||||
data-large-emojis={useLargeEmojis}
|
||||
onClick={ev => {
|
||||
if (ev.target) {
|
||||
let element: Element = ev.target as any;
|
||||
if (element.classList.contains("spoiler")) {
|
||||
element.classList.add("shown");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,4 +5,5 @@ export default styled.div`
|
||||
display: flex;
|
||||
user-select: none;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import Banner from "../../ui/Banner";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { AppContext, ClientStatus } from "../../../context/revoltjs/RevoltClient";
|
||||
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
export default function ConnectionStatus() {
|
||||
const { status } = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
if (status === ClientStatus.OFFLINE) {
|
||||
return (
|
||||
|
||||
@@ -48,7 +48,7 @@ const HomeList = styled.div`
|
||||
|
||||
function HomeSidebar(props: Props) {
|
||||
const { pathname } = useLocation();
|
||||
const { client } = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
// const { openScreen, writeClipboard } = useContext(IntermediateContext);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ const Checkmark = styled.div<{ checked: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
export interface CheckboxProps {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
@@ -69,7 +69,7 @@ interface Props {
|
||||
onChange: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Checkbox(props: Props) {
|
||||
export default function Checkbox(props: CheckboxProps) {
|
||||
return (
|
||||
<CheckboxBase disabled={props.disabled}>
|
||||
<CheckboxContent>
|
||||
|
||||
138
src/components/ui/Modal.tsx
Normal file
138
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import Button from "./Button";
|
||||
import classNames from "classnames";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { createPortal, useEffect } from "preact/compat";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
const open = keyframes`
|
||||
0% {opacity: 0;}
|
||||
70% {opacity: 0;}
|
||||
100% {opacity: 1;}
|
||||
`;
|
||||
|
||||
const zoomIn = keyframes`
|
||||
0% {transform: scale(0.5);}
|
||||
98% {transform: scale(1.01);}
|
||||
100% {transform: scale(1);}
|
||||
`;
|
||||
|
||||
const ModalBase = styled.div`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
max-height: 100%;
|
||||
user-select: none;
|
||||
|
||||
animation-name: ${open};
|
||||
animation-duration: 0.2s;
|
||||
|
||||
display: grid;
|
||||
overflow-y: auto;
|
||||
place-items: center;
|
||||
|
||||
color: var(--foreground);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
`;
|
||||
|
||||
const ModalContainer = styled.div`
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
max-width: calc(100vw - 20px);
|
||||
|
||||
animation-name: ${zoomIn};
|
||||
animation-duration: 0.25s;
|
||||
animation-timing-function: cubic-bezier(.3,.3,.18,1.1);
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>`
|
||||
`;
|
||||
|
||||
const ModalActions = styled.div`
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
padding: 1em 1.5em;
|
||||
border-radius: 0 0 8px 8px;
|
||||
background: var(--secondary-background);
|
||||
`;
|
||||
|
||||
export interface Action {
|
||||
text: Children;
|
||||
onClick: () => void;
|
||||
confirmation?: boolean;
|
||||
style?: 'default' | 'contrast' | 'error' | 'contrast-error';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children?: Children;
|
||||
title?: Children;
|
||||
|
||||
disallowClosing?: boolean;
|
||||
noBackground?: boolean;
|
||||
dontModal?: boolean;
|
||||
|
||||
onClose: () => void;
|
||||
actions?: Action[];
|
||||
disabled?: boolean;
|
||||
border?: boolean;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default function Modal(props: Props) {
|
||||
if (!props.visible) return null;
|
||||
|
||||
let content = (
|
||||
<ModalContent
|
||||
attachment={!!props.actions}
|
||||
noBackground={props.noBackground}
|
||||
border={props.border}>
|
||||
{props.title && <h3>{props.title}</h3>}
|
||||
{props.children}
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
if (props.dontModal) {
|
||||
return content;
|
||||
}
|
||||
|
||||
let confirmationAction = props.actions?.find(action => action.confirmation);
|
||||
useEffect(() => {
|
||||
if (!confirmationAction) return;
|
||||
|
||||
// ! FIXME: this may be done better if we
|
||||
// ! can focus the button although that
|
||||
// ! doesn't seem to work...
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
confirmationAction!.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, [ confirmationAction ]);
|
||||
|
||||
return createPortal(
|
||||
<ModalBase onClick={(!props.disallowClosing && props.onClose) || undefined}>
|
||||
<ModalContainer onClick={e => (e.cancelBubble = true)}>
|
||||
{content}
|
||||
{props.actions && (
|
||||
<ModalActions>
|
||||
{props.actions.map(x => (
|
||||
<Button style={x.style ?? "contrast"}
|
||||
onClick={x.onClick}
|
||||
disabled={props.disabled}>
|
||||
{x.text}
|
||||
</Button>
|
||||
))}
|
||||
</ModalActions>
|
||||
)}
|
||||
</ModalContainer>
|
||||
</ModalBase>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user