import React, { useEffect, useState, Fragment, isValidElement, useCallback, } from "react"; import type { ReactNode, ReactElement } from "react"; import { Box, Button, Flex, Input, Label, 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: "$error", mt: "$1", display: "block", }); interface TabProps { header?: string; children: ReactNode; } // TODO customise messages shown interface Props { activeIndex?: number; activeHeader?: string; headless?: boolean; children: ReactElement[]; keepAllAlive?: boolean; defaultExtension?: string; forceDefaultExtension?: boolean; onCreateNewTab?: (name: string) => any; onCloseTab?: (index: number, header?: string) => any; onChangeActive?: (index: number, header?: string) => any; } export const Tab = (props: TabProps) => null; export const Tabs = ({ children, activeIndex, activeHeader, headless, keepAllAlive = false, onCreateNewTab, onCloseTab, onChangeActive, defaultExtension = "", forceDefaultExtension, }: Props) => { const [active, setActive] = useState(activeIndex || 0); 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); }, [activeIndex]); useEffect(() => { if (activeHeader) { const idx = tabs.findIndex(tab => tab.header === activeHeader); if (idx !== -1) setActive(idx); else setActive(0); } }, [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 handleActiveChange = useCallback( (idx: number, header?: string) => { setActive(idx); onChangeActive?.(idx, header); }, [onChangeActive] ); const handleCreateTab = useCallback(() => { // add default extension in case omitted let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension; if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) { _tabname = _tabname + defaultExtension; } const chk = validateTabname(_tabname); if (chk.error) { setNewtabError(`Error: ${chk.error}`); return; } setIsNewtabDialogOpen(false); setTabname(""); onCreateNewTab?.(_tabname); // switch to new tab? handleActiveChange(tabs.length, _tabname); }, [ tabname, defaultExtension, forceDefaultExtension, validateTabname, onCreateNewTab, handleActiveChange, tabs.length, ]); const handleCloseTab = useCallback( (idx: number) => { if (idx <= active && active !== 0) { setActive(active - 1); } onCloseTab?.(idx, tabs[idx].header); }, [active, onCloseTab, tabs] ); return ( <> {!headless && ( {tabs.map((tab, idx) => ( ))} {onCreateNewTab && ( Create new tab setTabname(e.target.value)} onKeyPress={e => { if (e.key === "Enter") { handleCreateTab(); } }} /> {newtabError} )} )} {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} )} ); };