From 7ea3d5d4bd46fab976bc8aa46d1490bf85f2cf47 Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Mon, 9 Jan 2023 12:03:13 -0500 Subject: [PATCH] Add Layout component Change-type: minor --- src/components/Layout/Header.tsx | 55 +++++++ src/components/Layout/Layout.stories.tsx | 42 ++++++ src/components/Layout/index.tsx | 175 +++++++++++++++++++++++ src/index.ts | 2 + 4 files changed, 274 insertions(+) create mode 100644 src/components/Layout/Header.tsx create mode 100644 src/components/Layout/Layout.stories.tsx create mode 100644 src/components/Layout/index.tsx diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx new file mode 100644 index 000000000..12f4c4661 --- /dev/null +++ b/src/components/Layout/Header.tsx @@ -0,0 +1,55 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as React from 'react'; +import theme from '../../theme'; +import { Box, BoxProps } from '../Box'; +import { Flex } from '../Flex'; +import { Button } from '../Button'; +import { faBars } from '@fortawesome/free-solid-svg-icons/faBars'; +import styled from 'styled-components'; + +const BaseHeader = styled(Box)` + z-index: 12; + position: relative; + width: 100%; +`; + +export interface HeaderProps extends BoxProps { + rightContent?: JSX.Element[]; + leftContent?: JSX.Element[]; + isSidebarCollapsed?: boolean; + setIsSidebarCollapsed?: (isSidebarCollapsed: boolean) => void; +} + +export const Header = ({ + rightContent, + leftContent, + isSidebarCollapsed, + setIsSidebarCollapsed, + ...props +}: HeaderProps) => ( + + + + {setIsSidebarCollapsed && ( + + )} + {leftContent} + + + {rightContent} + + + +); diff --git a/src/components/Layout/Layout.stories.tsx b/src/components/Layout/Layout.stories.tsx new file mode 100644 index 000000000..8605b0693 --- /dev/null +++ b/src/components/Layout/Layout.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Meta } from '@storybook/react'; +import { createTemplate, createStory } from '../../stories/utils'; +import { Layout, LayoutProps } from '.'; +import { Box } from '../Box'; +import { Txt } from '../Txt'; + +export default { + title: 'Core/Layout', + component: Layout, +} as Meta; + +const Template = createTemplate(Layout); + +export const Default = createStory(Template, { + header: { + leftContent: ( + + This is a header + + ), + }, + leftSider: { + menuItems: [ + { title: 'foo', href: 'roadmap.balena.io' }, + { title: 'bar', href: 'docs.balena.io' }, + ], + navLink: Txt, + minHeight: '100vh', + minWidth: '10vw', + }, + children: ( + + The Layout component can give your product a sidebar and a header by + passing respective props for either component, or you can pass your own + into it. It accepts children (in this case the text you're seeing here), + as well as components for the Footer, header for the content (children), + and error boundary (if one is not provided, a native rendition one will be + used). + + ), +}); diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx new file mode 100644 index 000000000..b9025536e --- /dev/null +++ b/src/components/Layout/index.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { useBreakpoint } from '../../hooks/useBreakpoint'; +import { default as InternalErrorBoundary } from '../../internal/ErrorBoundary'; +import { Box } from '../Box'; +import { Sidebar, SidebarProps } from '../Sidebar'; +import { Header, HeaderProps } from './Header'; + +export interface LayoutProps { + header?: + | JSX.Element + | HeaderProps + | (( + isSidebarCollapsed?: boolean, + setIsSidebarCollapsed?: (state: boolean) => void, + ) => JSX.Element); + footer?: JSX.Element; + leftSider?: + | JSX.Element + | SidebarProps + | (( + isSidebarCollapsed?: boolean, + setIsSidebarCollapsed?: (state: boolean) => void, + ) => JSX.Element); + contentHeader?: JSX.Element; + children: JSX.Element; + ErrorBoundary?: any; // TODO + isSidebarCollapsed?: boolean; + setIsSidebarCollapsed?: (state: boolean) => void; +} + +const Grid = styled(Box)<{ + gridTemplateAreas: string; + gridTemplateRows: string; + gridTemplateColumns: string; +}>` + display: grid; + grid-template-areas: ${(props) => props.gridTemplateAreas}; + grid-template-rows: ${(props) => props.gridTemplateRows}; + grid-template-columns: ${(props) => props.gridTemplateColumns}; +`; + +const GridItem = styled(Box)<{ gridArea: string }>` + grid-area: ${(props) => props.gridArea}; +`; + +export const Layout = ({ + header, + footer, + leftSider, + contentHeader, + children, + ErrorBoundary, + isSidebarCollapsed, + setIsSidebarCollapsed, +}: LayoutProps) => { + const templateAreas = ` + "leftSider header" + "leftSider contentHeader" + "leftSider content" + "footer footer" + `; + + const templateRows = ` + [header header] max-content + [contentHeader] max-content + [content] minmax(0,1fr) + [footer footer] max-content + `; + + const templateColumns = ` + [leftSider] max-content + [content] minmax(0, 1fr) + `; + + const mobile = useBreakpoint([true, true, false]); + const [collapsed, setCollapsed] = React.useState(true); + + React.useEffect(() => { + if (mobile) { + (setIsSidebarCollapsed ?? setCollapsed)(true); + } else { + (setIsSidebarCollapsed ?? setCollapsed)(false); + } + }, [mobile]); + + const ErrorBoundaryComponent = React.useCallback( + ({ children }: { children: JSX.Element }) => { + return ErrorBoundary ? ( + {children} + ) : ( + {children} + ); + }, + [ErrorBoundary], + ); + + return ( + + {header && ( + + + {React.isValidElement(header) ? ( + header + ) : typeof header === 'object' ? ( +
+ ) : ( + header( + isSidebarCollapsed ?? collapsed, + setIsSidebarCollapsed ?? setCollapsed, + ) + )} + + + )} + {leftSider && ( + + {React.isValidElement(leftSider) ? ( + leftSider + ) : typeof leftSider === 'object' ? ( + + + + ) : ( + leftSider( + isSidebarCollapsed ?? collapsed, + setIsSidebarCollapsed ?? setCollapsed, + ) + )} + + )} + {contentHeader && ( + + {{contentHeader}} + + )} + + {children} + + {footer && ( + + {footer} + + )} + + ); +}; diff --git a/src/index.ts b/src/index.ts index 646dcc6cc..a48f7645f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,8 @@ export { Provider } from './components/Provider'; export { Rating, RatingProps } from './components/Rating'; export { Search, SearchProps } from './components/Search'; export { Select, SelectProps } from './components/Select'; +export { Layout, LayoutProps } from './components/Layout'; +export { Header, HeaderProps } from './components/Layout/Header'; export { Sidebar, SidebarProps } from './components/Sidebar'; export { Spinner, SpinnerProps } from './components/Spinner'; export {