Commit 0f94a75f authored by tom's avatar tom

side bar tabs and collapse button

parent 06b2d672
......@@ -4,6 +4,7 @@ import React from 'react';
import type { File } from './types';
import CodeEditorFileTree from './CodeEditorFileTree';
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
import composeFileTree from './utils/composeFileTree';
interface Props {
......@@ -12,6 +13,7 @@ interface Props {
}
const CodeEditorFileExplorer = ({ data, onFileSelect }: Props) => {
const [ key, setKey ] = React.useState(0);
const tree = React.useMemo(() => {
return composeFileTree(data);
}, [ data ]);
......@@ -25,9 +27,14 @@ const CodeEditorFileExplorer = ({ data, onFileSelect }: Props) => {
}
}, [ data, onFileSelect ]);
const handleCollapseButtonClick = React.useCallback(() => {
setKey((prev) => prev + 1);
}, []);
return (
<Box>
<CodeEditorFileTree tree={ tree } onItemClick={ handleFileClick }/>
<CoderEditorCollapseButton onClick={ handleCollapseButtonClick } label="Collapse folders"/>
<CodeEditorFileTree key={ key } tree={ tree } onItemClick={ handleFileClick } isCollapsed={ key > 0 }/>
</Box>
);
};
......
......@@ -7,10 +7,11 @@ import type { FileTree } from './types';
interface Props {
tree: FileTree;
level?: number;
isCollapsed?: boolean;
onItemClick: (event: React.MouseEvent) => void;
}
const CodeEditorFileTree = ({ tree, level = 0, onItemClick }: Props) => {
const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed }: Props) => {
const itemProps: ChakraProps = {
ml: level ? 4 : 0,
borderWidth: '0px',
......@@ -21,7 +22,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick }: Props) => {
};
return (
<Accordion allowMultiple defaultIndex={ tree.map((item, index) => index) } reduceMotion>
<Accordion allowMultiple defaultIndex={ isCollapsed ? undefined : tree.map((item, index) => index) } reduceMotion>
{
tree.map((leaf, index) => {
if ('children' in leaf) {
......@@ -32,7 +33,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick }: Props) => {
<span>{ leaf.name }</span>
</AccordionButton>
<AccordionPanel p={ 0 }>
<CodeEditorFileTree tree={ leaf.children } level={ level + 1 } onItemClick={ onItemClick }/>
<CodeEditorFileTree tree={ leaf.children } level={ level + 1 } onItemClick={ onItemClick } isCollapsed={ isCollapsed }/>
</AccordionPanel>
</AccordionItem>
);
......
......@@ -6,6 +6,7 @@ import type { File, Monaco, SearchResult } from './types';
import useDebounce from 'lib/hooks/useDebounce';
import CodeEditorSearchSection from './CodeEditorSearchSection';
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
interface Props {
data: Array<File>;
......@@ -16,6 +17,7 @@ interface Props {
const CodeEditorSearch = ({ monaco, data, onFileSelect }: Props) => {
const [ searchTerm, changeSearchTerm ] = React.useState('');
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const debouncedSearchTerm = useDebounce(searchTerm, 300);
......@@ -41,6 +43,10 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect }: Props) => {
setSearchResults(result.length > 0 ? result : []);
}, [ debouncedSearchTerm, monaco ]);
React.useEffect(() => {
setExpandedSections(searchResults.map((item, index) => index));
}, [ searchResults ]);
const handleSearchTermChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
changeSearchTerm(event.target.value);
}, []);
......@@ -52,13 +58,31 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect }: Props) => {
}
}, [ data, onFileSelect ]);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue);
}, []);
const handleToggleCollapseClick = React.useCallback(() => {
if (expandedSections.length === 0) {
setExpandedSections(searchResults.map((item, index) => index));
} else {
setExpandedSections([]);
}
}, [ expandedSections.length, searchResults ]);
return (
<Box>
<CoderEditorCollapseButton
onClick={ handleToggleCollapseClick }
label={ expandedSections.length === 0 ? 'Expand all' : 'Collapse all' }
isDisabled={ searchResults.length === 0 }
/>
<Input size="xs" onChange={ handleSearchTermChange } value={ searchTerm } placeholder="Search"/>
<Accordion
key={ debouncedSearchTerm }
allowMultiple
defaultIndex={ searchResults.map((item, index) => index) }
index={ expandedSections }
onChange={ handleAccordionStateChange }
reduceMotion
mt={ 3 }
>
......
import { Box, Flex, Tooltip } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import React from 'react';
import type { File, Monaco } from './types';
......@@ -13,40 +14,37 @@ interface Props {
}
const CodeEditorSideBar = ({ onFileSelect, data, monaco }: Props) => {
const [ activePanelIndex, setActivePanelIndex ] = React.useState(0);
const PANELS = React.useMemo(() => ([
{ id: 'explorer', label: 'Explorer', text: 'E', component: <CodeEditorFileExplorer data={ data } onFileSelect={ onFileSelect }/> },
{ id: 'search', label: 'Search', text: 'S', component: <CodeEditorSearch data={ data } onFileSelect={ onFileSelect } monaco={ monaco }/> },
]), [ data, monaco, onFileSelect ]);
const activePanel = PANELS[activePanelIndex];
const handleTabClick = React.useCallback((event: React.MouseEvent) => {
const id = (event.currentTarget as HTMLDivElement).getAttribute('data-id');
const index = PANELS.findIndex((item) => item.id === id);
if (index > -1) {
setActivePanelIndex(index);
}
}, [ PANELS ]);
const tabProps: HTMLChakraProps<'button'> = {
fontFamily: 'heading',
textTransform: 'uppercase',
fontSize: 'xs',
fontWeight: 500,
color: 'gray.600',
_selected: {
color: 'black',
},
px: 0,
letterSpacing: 0.3,
};
return (
<Box w="250px" flexShrink={ 0 } bgColor="lightpink" fontSize="sm" overflowY="scroll" px={ 3 }>
<Flex>
<Box fontFamily="heading" letterSpacing={ 0.5 } fontWeight={ 600 } textTransform="uppercase" lineHeight={ 6 }>
{ activePanel.label }
</Box>
<Flex ml="auto" columnGap={ 2 }>
{ PANELS.map(({ id, text, label }) => (
<Tooltip key={ id } label={ label }>
<Box data-id={ id } boxSize={ 6 } cursor="pointer" textAlign="center" bgColor="lightblue" onClick={ handleTabClick }>
{ text }
</Box>
</Tooltip>
)) }
</Flex>
</Flex>
{ activePanel.component }
<Box w="250px" flexShrink={ 0 } bgColor="lightpink" fontSize="sm" overflowY="scroll" px={ 3 } position="relative">
<Tabs isLazy lazyBehavior="keepMounted" variant="unstyled" size="sm">
<TabList columnGap={ 3 }>
<Tab { ...tabProps }>Explorer</Tab>
<Tab { ...tabProps }>Search</Tab>
</TabList>
<TabPanels>
<TabPanel p={ 0 }>
<CodeEditorFileExplorer data={ data } onFileSelect={ onFileSelect }/>
</TabPanel>
<TabPanel p={ 0 }>
<CodeEditorSearch data={ data } onFileSelect={ onFileSelect } monaco={ monaco }/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};
......
import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import iconCopy from 'icons/copy.svg';
interface Props {
onClick: () => void;
label: string;
isDisabled?: boolean;
}
const CoderEditorCollapseButton = ({ onClick, label, isDisabled }: Props) => {
return (
<Tooltip label={ label } isDisabled={ isDisabled }>
<Flex position="absolute" right={ 3 } top={ 2 }>
<IconButton
as={ iconCopy }
boxSize={ 4 }
variant="unstyled"
cursor="pointer"
aria-label="collapse"
onClick={ onClick }
isDisabled={ isDisabled }
/>
</Flex>
</Tooltip>
);
};
export default React.memo(CoderEditorCollapseButton);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment