diff --git a/components/Tabs.tsx b/components/Tabs.tsx index bfa74d4..1a351ea 100644 --- a/components/Tabs.tsx +++ b/components/Tabs.tsx @@ -1,24 +1,58 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState, Fragment, isValidElement, useCallback } from "react"; import type { ReactNode, ReactElement } from "react"; -import { Button, Stack } from "."; +import { Box, Button, Flex, Input, Stack, Text } from "."; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogTitle, + DialogDescription, + DialogClose, +} from "./Dialog"; +import { Plus, X } from "phosphor-react"; +import { styled } from "../stitches.config"; + +const ErrorText = styled(Text, { + color: "$red9", + mt: "$1", + display: "block", +}); interface TabProps { header?: string; children: ReactNode; } +// TODO customise strings shown interface Props { activeIndex?: number; activeHeader?: string; headless?: boolean; children: ReactElement[]; + keepAllAlive?: boolean; + defaultExtension?: string; + onCreateNewTab?: (name: string) => any; + onCloseTab?: (index: number, header?: string) => any; } export const Tab = (props: TabProps) => null; -export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) => { +export const Tabs = ({ + children, + activeIndex, + activeHeader, + headless, + keepAllAlive = false, + onCreateNewTab, + onCloseTab, + defaultExtension = "", +}: Props) => { const [active, setActive] = useState(activeIndex || 0); - const tabProps: TabProps[] = children.map(elem => elem.props); + const tabs: TabProps[] = children.map(elem => elem.props); + + const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false); + const [tabname, setTabname] = useState(""); + const [newtabError, setNewtabError] = useState(null); useEffect(() => { if (activeIndex) setActive(activeIndex); @@ -26,10 +60,53 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) = useEffect(() => { if (activeHeader) { - const idx = tabProps.findIndex(tab => tab.header === activeHeader); + const idx = tabs.findIndex(tab => tab.header === activeHeader); setActive(idx); } - }, [activeHeader, tabProps]); + }, [activeHeader, tabs]); + + // when filename changes, reset error + useEffect(() => { + setNewtabError(null); + }, [tabname, setNewtabError]); + + const validateTabname = useCallback( + (tabname: string): { error: string | null } => { + if (tabs.find(tab => tab.header === tabname)) { + return { error: "Name already exists." }; + } + return { error: null }; + }, + [tabs] + ); + + const handleCreateTab = useCallback(() => { + // add default extension in case omitted + let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension; + const chk = validateTabname(_tabname); + if (chk.error) { + setNewtabError(`Error: ${chk.error}`); + return; + } + + setIsNewtabDialogOpen(false); + setTabname(""); + // switch to new tab? + setActive(tabs.length); + + onCreateNewTab?.(_tabname); + }, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]); + + const handleCloseTab = useCallback( + (idx: number) => { + if (idx <= active && active !== 0) { + setActive(active - 1); + } + + onCloseTab?.(idx, tabs[idx].header); + }, + [active, onCloseTab, tabs] + ); return ( <> @@ -42,9 +119,9 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) = marginBottom: "-1px", }} > - {tabProps.map((prop, idx) => ( + {tabs.map((tab, idx) => ( ))} + {onCreateNewTab && ( + + + + + + Create new tab + + + setTabname(e.target.value)} + onKeyPress={e => { + if (e.key === "Enter") { + handleCreateTab(); + } + }} + /> + {newtabError} + + + + + + + + + + + + + + + + )} )} - {tabProps[active].children} + {keepAllAlive ? ( + tabs.map((tab, idx) => { + // TODO Maybe rule out fragments as children + if (!isValidElement(tab.children)) { + if (active !== idx) return null; + return tab.children; + } + let key = tab.children.key || tab.header || idx; + let { children } = tab; + let { style, ...props } = children.props; + return ( + + ); + }) + ) : ( + {tabs[active].children} + )} ); }; diff --git a/pages/test/[[...slug]].tsx b/pages/test/[[...slug]].tsx index 79416d8..dd122b7 100644 --- a/pages/test/[[...slug]].tsx +++ b/pages/test/[[...slug]].tsx @@ -4,7 +4,7 @@ import dynamic from "next/dynamic"; import { useSnapshot } from "valtio"; import state from "../../state"; import { sendTransaction } from "../../state/actions"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, FC } from "react"; import transactionsData from "../../content/transactions.json"; const LogBox = dynamic(() => import("../../components/LogBox"), { @@ -18,7 +18,7 @@ const Accounts = dynamic(() => import("../../components/Accounts"), { type TxFields = Omit; type OtherFields = (keyof Omit)[]; -const Transaction = () => { +const Transaction: FC = props => { const snap = useSnapshot(state); const transactionsOptions = transactionsData.map(tx => ({ @@ -150,7 +150,7 @@ const Transaction = () => { const usualFields = ["TransactionType", "Amount", "Account", "Destination"]; const otherFields = Object.keys(txFields).filter(k => !usualFields.includes(k)) as OtherFields; return ( - + @@ -280,6 +280,7 @@ const Transaction = () => { const Test = () => { const snap = useSnapshot(state); + const [tabHeaders, setTabHeaders] = useState(["test1.json"]); return ( { css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }} > - - {/* TODO Dynamic tabs */} - - - - - - + setTabHeaders(tabHeaders.concat(name))} + onCloseTab={index => setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))} + > + {tabHeaders.map(header => ( + + + + ))}