diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f97927c5..a0af0264 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,10 +1,13 @@ import { type ComponentProps, + type Dispatch, type Ref, + type SetStateAction, createContext, forwardRef, useContext, useMemo, + useState, } from 'react' import styled from 'styled-components' @@ -14,10 +17,12 @@ type SidebarVariant = 'app' | 'console' type SidebarBaseProps = { layout?: SidebarLayout variant: SidebarVariant + isExpanded?: boolean + setIsExpanded?: Dispatch> } type SidebarProps = SidebarBaseProps & ComponentProps -const SIDEBAR_WIDTH = 64 +export const SIDEBAR_WIDTH = 64 const SIDEBAR_HEIGHT = 56 const SidebarContext = createContext(null) @@ -49,22 +54,24 @@ const SidebarSC = styled.div<{ : $variant === 'console' ? theme.colors.grey[950] : theme.colors['fill-one'], - - borderRight: $isHorizontal ? 'none' : theme.borders.default, borderBottom: $isHorizontal ? theme.borders.default : 'none', - overflowY: 'hidden', + overflow: 'visible', })) function SidebarRef( { layout = 'vertical', variant = 'app', ...props }: SidebarProps, ref: Ref ) { + const [isExpanded, setIsExpanded] = useState(false) + const contextVal = useMemo( () => ({ layout, variant, + isExpanded, + setIsExpanded, }), - [layout, variant] + [layout, variant, isExpanded, setIsExpanded] ) return ( @@ -72,6 +79,7 @@ function SidebarRef( diff --git a/src/components/SidebarExpandButton.tsx b/src/components/SidebarExpandButton.tsx new file mode 100644 index 00000000..97bd647f --- /dev/null +++ b/src/components/SidebarExpandButton.tsx @@ -0,0 +1,27 @@ +import { type MouseEvent } from 'react' + +import { HamburgerMenuCollapseIcon, HamburgerMenuIcon } from '../icons' + +import { useSidebar } from './Sidebar' +import SidebarItem from './SidebarItem' + +function SidebarExpandButton() { + const { setIsExpanded, isExpanded } = useSidebar() + + return ( + { + e.stopPropagation() + setIsExpanded((x: boolean) => !x) + }} + > + {isExpanded ? : } + + ) +} + +export default SidebarExpandButton diff --git a/src/components/SidebarExpandWrapper.tsx b/src/components/SidebarExpandWrapper.tsx new file mode 100644 index 00000000..17ccdfdb --- /dev/null +++ b/src/components/SidebarExpandWrapper.tsx @@ -0,0 +1,63 @@ +import { useOutsideClick } from 'honorable' +import { type ReactNode, useEffect, useRef } from 'react' + +import styled from 'styled-components' + +import { SIDEBAR_WIDTH, useSidebar } from './Sidebar' + +export const SIDEBAR_EXPANDED_WIDTH = 180 + +function SidebarExpandWrapper({ + pathname, + ...props +}: { + pathname?: string + children: ReactNode[] +}) { + const { layout, isExpanded, setIsExpanded } = useSidebar() + const isHorizontal = layout === 'horizontal' + const ref = useRef(null) + + useOutsideClick(ref, () => { + setIsExpanded(false) + }) + + useEffect(() => { + setIsExpanded(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname]) + + return ( + + ) +} + +export default SidebarExpandWrapper + +const Animated = styled.div<{ + $isExpanded: boolean + $isHorizontal: boolean +}>(({ theme, $isExpanded, $isHorizontal }) => ({ + display: 'flex', + flexDirection: $isHorizontal ? 'row' : 'column', + position: 'relative', + alignItems: 'center', + borderBottom: $isHorizontal ? '' : `1px solid ${theme.colors.border}`, + gap: $isHorizontal ? theme.spacing.medium : theme.spacing.xsmall, + borderRight: $isHorizontal ? undefined : `1px solid ${theme.colors.border}`, + width: $isExpanded ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_WIDTH, + minWidth: $isExpanded ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_WIDTH, + maxWidth: $isExpanded ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_WIDTH, + height: '100%', + backgroundColor: 'inherit', + transition: 'all 0.2s ease', + zIndex: 10, + ':last-of-type': { + borderBottom: 'none', + }, +})) diff --git a/src/components/SidebarItem.tsx b/src/components/SidebarItem.tsx index 89fbea62..b332c9d2 100644 --- a/src/components/SidebarItem.tsx +++ b/src/components/SidebarItem.tsx @@ -8,22 +8,27 @@ import { type SidebarVariant, useSidebar } from './Sidebar' type SidebarItemProps = ComponentProps & { clickable?: boolean tooltip?: string + expandedLabel?: string active?: boolean } -function SidebarItemRef( - { children, clickable = false, tooltip = '', ...props }: SidebarItemProps, - ref: Ref -) { +function SidebarItemRef({ + children, + tooltip = '', + expandedLabel = '', + className, + ...props +}: SidebarItemProps) { + const { isExpanded } = useSidebar() + return ( - - - {children} - + + {children} + {isExpanded && expandedLabel ? expandedLabel : null} ) } @@ -32,9 +37,19 @@ function WithTooltipRef( { children, clickable, tooltip = '', ...props }: SidebarItemProps, ref: Ref ) { - const { layout } = useSidebar() + const { layout, isExpanded } = useSidebar() - if (!tooltip) return <> {children} + if (!tooltip || isExpanded) + return ( + + {children} + + ) return ( (({ theme, $clickable, $active, $isHorizontal, $variant }) => ({ display: 'flex', alignItems: 'center', - justifyContent: 'center', - width: $isHorizontal ? undefined : 32, - height: 32, + justifyContent: 'flex-start', + gap: theme.spacing.xsmall, + textDecoration: 'none', + whiteSpace: 'nowrap', + width: $isHorizontal ? undefined : '100%', + height: $isHorizontal ? undefined : 40, flexGrow: 0, + padding: $isHorizontal ? undefined : theme.spacing.small, borderRadius: '3px', overflow: 'hidden', color: theme.colors['icon-light'], diff --git a/src/components/SidebarSection.tsx b/src/components/SidebarSection.tsx index de8136d2..7b8cc9b9 100644 --- a/src/components/SidebarSection.tsx +++ b/src/components/SidebarSection.tsx @@ -29,6 +29,7 @@ function SidebarSectionRef( borderBottom={isHorizontal ? '' : '1px solid border'} gap={isHorizontal ? 'medium' : 'xsmall'} padding={12} + width={isHorizontal ? 'auto' : '100%'} {...styles} {...props} /> diff --git a/src/index.ts b/src/index.ts index 0f670d7a..ab62b313 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,8 +68,13 @@ export { export type { TooltipProps } from './components/Tooltip' export { default as Tooltip } from './components/Tooltip' export { default as FormTitle } from './components/FormTitle' -export { default as Sidebar } from './components/Sidebar' +export { default as Sidebar, SIDEBAR_WIDTH } from './components/Sidebar' export { default as SidebarSection } from './components/SidebarSection' +export { default as SidebarExpandButton } from './components/SidebarExpandButton' +export { + default as SidebarExpandWrapper, + SIDEBAR_EXPANDED_WIDTH, +} from './components/SidebarExpandWrapper' export { default as SidebarItem } from './components/SidebarItem' export { default as Modal } from './components/Modal' export { default as Flyover } from './components/Flyover' diff --git a/src/stories/Sidebar.stories.tsx b/src/stories/Sidebar.stories.tsx index 6d43c448..fd69d4e5 100644 --- a/src/stories/Sidebar.stories.tsx +++ b/src/stories/Sidebar.stories.tsx @@ -15,6 +15,8 @@ import Sidebar from '../components/Sidebar' import SidebarItem from '../components/SidebarItem' import SidebarSection from '../components/SidebarSection' import { Button, IconFrame, PluralLogoMark } from '../index' +import SidebarExpandButton from '../components/SidebarExpandButton' +import SidebarExpandWrapper from '../components/SidebarExpandWrapper' export default { title: 'Sidebar', @@ -59,71 +61,82 @@ function Template({ variant }: any) { return (
- - - - - - - - {items.map(({ tooltip, icon }) => ( + + { - e.preventDefault() - setActiveKey(tooltip) - }} - key={tooltip} - tooltip={tooltip} - active={tooltip === activeKey} + href="https://app.plural.sh" + expandedLabel="Plural" > - {icon} + - ))} - + + + {items.map(({ tooltip, icon }) => ( + { + e.preventDefault() + setActiveKey(tooltip) + }} + key={tooltip} + tooltip={tooltip} + active={tooltip === activeKey} + expandedLabel={tooltip} + > + {icon} + + ))} + - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + +
)