Convert to typescript
This commit is contained in:
23
src/components/Button.tsx
Normal file
23
src/components/Button.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Button = styled.div`
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.25em;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
:hover {
|
||||
background: ${props => props.theme.highlight};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Button;
|
||||
@@ -1,11 +1,21 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ThemeProvider, createGlobalStyle } from 'styled-components';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
import ls from 'local-storage';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import usePreference from '../hooks/usePreference';
|
||||
import themes, { Themes } from '../style/themes';
|
||||
import EditorControls from './EditorControls';
|
||||
import EditorGlobalStyle from './EditorGlobalStyle';
|
||||
import EditorTextArea from './EditorTextArea';
|
||||
import themes from '../style/themes';
|
||||
|
||||
export interface EditorProps {
|
||||
forcedContent: string;
|
||||
setForcedContent: (value: string) => void;
|
||||
actualContent: string;
|
||||
setActualContent: (value: string) => void;
|
||||
contentType?: string;
|
||||
pasteId?: string;
|
||||
}
|
||||
|
||||
export default function Editor({
|
||||
forcedContent,
|
||||
@@ -14,16 +24,16 @@ export default function Editor({
|
||||
setActualContent,
|
||||
contentType,
|
||||
pasteId,
|
||||
}) {
|
||||
const [language, setLanguage] = useState('plain');
|
||||
const [readOnly, setReadOnly] = useState(isMobile && pasteId);
|
||||
}: EditorProps) {
|
||||
const [language, setLanguage] = useState<string>('plain');
|
||||
const [readOnly, setReadOnly] = useState<boolean>(isMobile && !!pasteId);
|
||||
|
||||
const [theme, setTheme] = usePreference(
|
||||
const [theme, setTheme] = usePreference<keyof Themes>(
|
||||
'theme',
|
||||
'dark',
|
||||
pref => !!themes[pref]
|
||||
);
|
||||
const [fontSize, setFontSize, fontSizeCheck] = usePreference(
|
||||
const [fontSize, setFontSize, fontSizeCheck] = usePreference<number>(
|
||||
'fontsize',
|
||||
16,
|
||||
pref => pref >= 10 && pref <= 22
|
||||
@@ -35,7 +45,7 @@ export default function Editor({
|
||||
}
|
||||
}, [contentType]);
|
||||
|
||||
function zoom(delta) {
|
||||
function zoom(delta: number) {
|
||||
const newFontSize = fontSize + delta;
|
||||
if (fontSizeCheck(newFontSize)) {
|
||||
setFontSize(newFontSize);
|
||||
@@ -65,39 +75,8 @@ export default function Editor({
|
||||
language={language}
|
||||
fontSize={fontSize}
|
||||
readOnly={readOnly}
|
||||
setReadOnly={setReadOnly}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const EditorGlobalStyle = createGlobalStyle`
|
||||
html, body {
|
||||
color-scheme: ${props => props.theme.lightOrDark};
|
||||
scrollbar-color: ${props => props.theme.lightOrDark};
|
||||
background-color: ${props => props.theme.editor.background};
|
||||
}
|
||||
`;
|
||||
|
||||
// hook used to load "preference" settings from local storage, or fall back to a default value.
|
||||
function usePreference(id, defaultValue, valid) {
|
||||
const [value, setValue] = useState(() => {
|
||||
const pref = ls.get(id);
|
||||
if (pref && valid(pref)) {
|
||||
return pref;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (value === defaultValue) {
|
||||
ls.remove(id);
|
||||
} else {
|
||||
ls.set(id, value);
|
||||
}
|
||||
}, [value, id, defaultValue]);
|
||||
|
||||
return [value, setValue, valid];
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { gzip } from 'pako';
|
||||
import history from 'history/browser';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import history from 'history/browser';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { MenuButton, Button } from './Menu';
|
||||
import themes, { Themes } from '../style/themes';
|
||||
import { languages } from '../util/highlighting';
|
||||
import themes from '../style/themes';
|
||||
import { postUrl } from '../util/constants';
|
||||
import { saveToBytebin } from '../util/storage';
|
||||
import Button from './Button';
|
||||
import MenuButton from './MenuButton';
|
||||
|
||||
export interface EditorControlsProps {
|
||||
actualContent: string;
|
||||
setForcedContent: (value: string) => void;
|
||||
language: string;
|
||||
setLanguage: (value: string) => void;
|
||||
readOnly: boolean;
|
||||
setReadOnly: (value: boolean) => void;
|
||||
theme: keyof Themes;
|
||||
setTheme: (value: keyof Themes) => void;
|
||||
zoom: (delta: number) => void;
|
||||
}
|
||||
|
||||
export default function EditorControls({
|
||||
actualContent,
|
||||
@@ -19,9 +31,9 @@ export default function EditorControls({
|
||||
theme,
|
||||
setTheme,
|
||||
zoom,
|
||||
}) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [recentlySaved, setRecentlySaved] = useState(false);
|
||||
}: EditorControlsProps) {
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [recentlySaved, setRecentlySaved] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRecentlySaved(false);
|
||||
@@ -46,7 +58,7 @@ export default function EditorControls({
|
||||
}, [actualContent, language, recentlySaved]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = e => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 's' || e.key === 'S') {
|
||||
e.preventDefault();
|
||||
@@ -100,7 +112,7 @@ export default function EditorControls({
|
||||
label="theme"
|
||||
value={theme}
|
||||
setValue={setTheme}
|
||||
ids={Object.keys(themes)}
|
||||
ids={Object.keys(themes) as (keyof Themes)[]}
|
||||
/>
|
||||
<Button
|
||||
className="optional"
|
||||
@@ -139,36 +151,3 @@ const Section = styled.div`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function langaugeToContentType(language) {
|
||||
if (language === 'json') {
|
||||
return 'application/json';
|
||||
} else {
|
||||
return 'text/' + language;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToBytebin(code, language) {
|
||||
try {
|
||||
const compressed = gzip(code);
|
||||
const contentType = langaugeToContentType(language);
|
||||
|
||||
const resp = await fetch(postUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Encoding': 'gzip',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: compressed,
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
const json = await resp.json();
|
||||
return json.key;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
12
src/components/EditorGlobalStyle.tsx
Normal file
12
src/components/EditorGlobalStyle.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createGlobalStyle, ThemeProps } from 'styled-components';
|
||||
import { Theme } from '../style/themes';
|
||||
|
||||
const EditorGlobalStyle = createGlobalStyle<ThemeProps<Theme>>`
|
||||
html, body {
|
||||
color-scheme: ${props => props.theme.lightOrDark};
|
||||
scrollbar-color: ${props => props.theme.lightOrDark};
|
||||
background-color: ${props => props.theme.editor.background};
|
||||
}
|
||||
`;
|
||||
|
||||
export default EditorGlobalStyle;
|
||||
@@ -1,8 +1,25 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Editor, {
|
||||
BeforeMount,
|
||||
Monaco,
|
||||
OnChange,
|
||||
OnMount,
|
||||
} from '@monaco-editor/react';
|
||||
import history from 'history/browser';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import themes, { makeMonacoTheme } from '../style/themes';
|
||||
import themes, { makeMonacoTheme, Theme } from '../style/themes';
|
||||
|
||||
import type { editor } from 'monaco-editor';
|
||||
|
||||
export interface EditorTextAreaProps {
|
||||
forcedContent: string;
|
||||
actualContent: string;
|
||||
setActualContent: (value: string) => void;
|
||||
theme: Theme;
|
||||
language: string;
|
||||
fontSize: number;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export default function EditorTextArea({
|
||||
forcedContent,
|
||||
@@ -12,11 +29,12 @@ export default function EditorTextArea({
|
||||
language,
|
||||
fontSize,
|
||||
readOnly,
|
||||
}) {
|
||||
const [editor, setEditor] = useState();
|
||||
const [monaco, setMonaco] = useState();
|
||||
}: EditorTextAreaProps) {
|
||||
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
|
||||
const [monaco, setMonaco] = useState<Monaco>();
|
||||
const [selected, toggleSelected] = useSelectedLine();
|
||||
const editorAreaRef = useRef();
|
||||
const editorAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLineNumberMagic(
|
||||
editorAreaRef,
|
||||
selected,
|
||||
@@ -25,9 +43,10 @@ export default function EditorTextArea({
|
||||
editor,
|
||||
monaco
|
||||
);
|
||||
|
||||
usePlaceholderText(actualContent, editor, monaco);
|
||||
|
||||
function beforeMount(monaco) {
|
||||
const beforeMount: BeforeMount = monaco => {
|
||||
for (const [id, theme] of Object.entries(themes)) {
|
||||
monaco.editor.defineTheme(id, makeMonacoTheme(theme));
|
||||
}
|
||||
@@ -36,32 +55,35 @@ export default function EditorTextArea({
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function onMount(editor, monaco) {
|
||||
const onMount: OnMount = (editor, monaco) => {
|
||||
editor.addAction({
|
||||
id: 'search',
|
||||
label: 'Search with Google',
|
||||
contextMenuGroupId: '9_cutcopypaste',
|
||||
contextMenuOrder: 5,
|
||||
run: (editor) => {
|
||||
const selection = editor.getSelection()
|
||||
if (!selection.isEmpty()) {
|
||||
const query = editor.getModel().getValueInRange(selection);
|
||||
window.open('https://www.google.com/search?q=' + query, '_blank');
|
||||
run: editor => {
|
||||
const selection = editor.getSelection();
|
||||
if (selection && !selection.isEmpty()) {
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
const query = model.getValueInRange(selection);
|
||||
window.open('https://www.google.com/search?q=' + query, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
setEditor(editor);
|
||||
setMonaco(monaco);
|
||||
|
||||
editor.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = useCallback(
|
||||
const onChange: OnChange = useCallback(
|
||||
value => {
|
||||
setActualContent(value);
|
||||
setActualContent(value as string);
|
||||
},
|
||||
[setActualContent]
|
||||
);
|
||||
@@ -72,8 +94,13 @@ export default function EditorTextArea({
|
||||
return;
|
||||
}
|
||||
|
||||
editor.getModel().detectIndentation(true, 2);
|
||||
}, [editor, forcedContent])
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.detectIndentation(true, 2);
|
||||
}, [editor, forcedContent]);
|
||||
|
||||
return (
|
||||
<EditorArea ref={editorAreaRef}>
|
||||
@@ -84,9 +111,9 @@ export default function EditorTextArea({
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: fontSize,
|
||||
fontLigatures: true,
|
||||
wordWrap: true,
|
||||
wordWrap: 'on',
|
||||
renderLineHighlight: 'none',
|
||||
renderValidationDecorations: false,
|
||||
renderValidationDecorations: 'off',
|
||||
readOnly,
|
||||
domReadOnly: readOnly,
|
||||
}}
|
||||
@@ -124,13 +151,17 @@ const EditorArea = styled.main`
|
||||
}
|
||||
`;
|
||||
|
||||
function usePlaceholderText(actualContent, editor, monaco) {
|
||||
function usePlaceholderText(
|
||||
actualContent: string,
|
||||
editor: editor.IStandaloneCodeEditor | undefined,
|
||||
monaco: Monaco | undefined
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!editor || !monaco || actualContent) {
|
||||
return;
|
||||
}
|
||||
const decoration = {
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
range: new monaco.Range(1, 0, 1, 1),
|
||||
options: {
|
||||
after: {
|
||||
content: '\u200B',
|
||||
@@ -146,13 +177,63 @@ function usePlaceholderText(actualContent, editor, monaco) {
|
||||
}, [editor, monaco, actualContent]);
|
||||
}
|
||||
|
||||
type SelectedLine = [number, number];
|
||||
type ToggleSelectedFunction = (lineNo: number, e: MouseEvent) => void;
|
||||
|
||||
function useSelectedLine(): [SelectedLine, ToggleSelectedFunction] {
|
||||
// extract highlighted lines from window hash
|
||||
const [selected, setSelected] = useState<[number, number]>(() => {
|
||||
const hash = window.location.hash;
|
||||
if (/^#L\d+(-\d+)?$/.test(hash)) {
|
||||
const [start, end] = hash.substring(2).split('-').map(Number);
|
||||
return [start, isNaN(end) ? start : end];
|
||||
} else {
|
||||
return [-1, -1];
|
||||
}
|
||||
});
|
||||
|
||||
// update window hash when a new line is highlighted
|
||||
useEffect(() => {
|
||||
let hash = '';
|
||||
|
||||
if (selected[0] !== -1) {
|
||||
if (selected[1] !== selected[0]) {
|
||||
const start = Math.min(...selected);
|
||||
const end = Math.max(...selected);
|
||||
hash = `#L${start}-${end}`;
|
||||
} else {
|
||||
hash = `#L${selected[0]}`;
|
||||
}
|
||||
}
|
||||
|
||||
history.replace({ hash });
|
||||
}, [selected]);
|
||||
|
||||
// toggle the highlighting for a given line
|
||||
const toggleSelected = useCallback(
|
||||
(lineNo: number, e: MouseEvent) => {
|
||||
const shift = e.shiftKey;
|
||||
if (selected[0] === lineNo && selected[1] === lineNo) {
|
||||
setSelected([-1, -1]);
|
||||
} else if (selected[0] === -1 || !shift) {
|
||||
setSelected([lineNo, lineNo]);
|
||||
} else {
|
||||
setSelected([selected[0], lineNo]);
|
||||
}
|
||||
},
|
||||
[selected, setSelected]
|
||||
);
|
||||
|
||||
return [selected, toggleSelected];
|
||||
}
|
||||
|
||||
function useLineNumberMagic(
|
||||
editorAreaRef,
|
||||
selected,
|
||||
toggleSelected,
|
||||
forcedContent,
|
||||
editor,
|
||||
monaco
|
||||
editorAreaRef: React.RefObject<HTMLDivElement>,
|
||||
selected: SelectedLine,
|
||||
toggleSelected: ToggleSelectedFunction,
|
||||
forcedContent: string,
|
||||
editor: editor.IStandaloneCodeEditor | undefined,
|
||||
monaco: Monaco | undefined
|
||||
) {
|
||||
// add an event listener for clicking on line numbers
|
||||
useEffect(() => {
|
||||
@@ -161,9 +242,13 @@ function useLineNumberMagic(
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = click => {
|
||||
const target = click?.target;
|
||||
if (target && target.classList.contains('line-numbers')) {
|
||||
const handler = (click: MouseEvent) => {
|
||||
const target = click?.target as HTMLElement;
|
||||
if (
|
||||
target &&
|
||||
target.classList.contains('line-numbers') &&
|
||||
target.textContent
|
||||
) {
|
||||
toggleSelected(parseInt(target.textContent), click);
|
||||
}
|
||||
};
|
||||
@@ -204,47 +289,3 @@ function useLineNumberMagic(
|
||||
};
|
||||
}, [editor, monaco, selected, forcedContent]);
|
||||
}
|
||||
|
||||
function useSelectedLine() {
|
||||
// extract highlighted lines from window hash
|
||||
const [selected, setSelected] = useState(() => {
|
||||
const hash = window.location.hash;
|
||||
if (/^#L\d+(-\d+)?$/.test(hash)) {
|
||||
const [start, end] = hash.substring(2).split('-').map(Number);
|
||||
return [start, isNaN(end) ? start : end];
|
||||
} else {
|
||||
return [-1, -1];
|
||||
}
|
||||
});
|
||||
|
||||
// update window hash when a new line is highlighted
|
||||
useEffect(() => {
|
||||
let hash = '';
|
||||
|
||||
if (selected[0] !== -1) {
|
||||
if (selected[1] !== selected[0]) {
|
||||
const start = Math.min(...selected);
|
||||
const end = Math.max(...selected);
|
||||
hash = `#L${start}-${end}`;
|
||||
} else {
|
||||
hash = `#L${selected[0]}`;
|
||||
}
|
||||
}
|
||||
|
||||
history.replace({ hash });
|
||||
}, [selected]);
|
||||
|
||||
// toggle the highlighting for a given line
|
||||
function toggleSelected(lineNo, e) {
|
||||
const shift = e.shiftKey;
|
||||
if (selected[0] === lineNo && selected[1] === lineNo) {
|
||||
setSelected([-1, -1]);
|
||||
} else if (selected[0] === -1 || !shift) {
|
||||
setSelected([lineNo, lineNo]);
|
||||
} else {
|
||||
setSelected([selected[0], lineNo]);
|
||||
}
|
||||
}
|
||||
|
||||
return [selected, toggleSelected];
|
||||
}
|
||||
@@ -1,25 +1,74 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Button from './Button';
|
||||
|
||||
export const Button = styled.div`
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.25em;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
interface MenuButtonProps<T extends string> {
|
||||
label: string;
|
||||
ids: T[] | Record<string, T[]>;
|
||||
value: T;
|
||||
setValue: (value: T) => void;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${props => props.theme.highlight};
|
||||
export default function MenuButton<T extends string>({
|
||||
label,
|
||||
ids,
|
||||
value,
|
||||
setValue,
|
||||
}: MenuButtonProps<T>) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
// close the menu when a click is made elsewhere
|
||||
useEffect(() => {
|
||||
const listener = (e: MouseEvent) => setOpen(false);
|
||||
window.addEventListener('click', listener);
|
||||
return () => window.removeEventListener('click', listener);
|
||||
}, [setOpen]);
|
||||
|
||||
function toggleOpen(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
setOpen(!open);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
function select(e: React.MouseEvent<HTMLLIElement>, id: T) {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
setValue(id);
|
||||
}
|
||||
`;
|
||||
|
||||
function make(ids: T[]) {
|
||||
return ids.map(id => (
|
||||
<li
|
||||
key={id}
|
||||
className={id === value ? 'selected' : undefined}
|
||||
onClick={e => select(e, id)}
|
||||
>
|
||||
{id}
|
||||
</li>
|
||||
));
|
||||
}
|
||||
|
||||
let items;
|
||||
if (Array.isArray(ids)) {
|
||||
items = make(ids);
|
||||
} else {
|
||||
items = Object.entries(ids).map(([title, values]) =>
|
||||
[
|
||||
<li className="title" key={title} onClick={e => e.stopPropagation()}>
|
||||
[{title}]
|
||||
</li>,
|
||||
]
|
||||
.concat(make(values))
|
||||
.flat()
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={toggleOpen}>
|
||||
[<span>{label}: </span>
|
||||
{value}]{open && <Menu>{items}</Menu>}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const Menu = styled.ul`
|
||||
position: absolute;
|
||||
@@ -60,58 +109,3 @@ const Menu = styled.ul`
|
||||
background-color: ${props => props.theme.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const MenuButton = ({ label, ids, value, setValue }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = e => setOpen(false);
|
||||
window.addEventListener('click', listener);
|
||||
return () => window.removeEventListener('click', listener);
|
||||
}, [setOpen]);
|
||||
|
||||
function toggleOpen(e) {
|
||||
e.stopPropagation();
|
||||
setOpen(!open);
|
||||
}
|
||||
|
||||
function select(e, id) {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
setValue(id);
|
||||
}
|
||||
|
||||
function make(ids) {
|
||||
return ids.map(id => (
|
||||
<li
|
||||
key={id}
|
||||
className={id === value ? 'selected' : undefined}
|
||||
onClick={e => select(e, id)}
|
||||
>
|
||||
{id}
|
||||
</li>
|
||||
));
|
||||
}
|
||||
|
||||
let items;
|
||||
if (Array.isArray(ids)) {
|
||||
items = make(ids);
|
||||
} else {
|
||||
items = Object.entries(ids).map(([title, values]) =>
|
||||
[
|
||||
<li className="title" key={title} onClick={e => e.stopPropagation()}>
|
||||
[{title}]
|
||||
</li>,
|
||||
]
|
||||
.concat(make(values))
|
||||
.flat()
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={toggleOpen}>
|
||||
[<span>{label}: </span>
|
||||
{value}]{open && <Menu>{items}</Menu>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user