Skip to content

Commit

Permalink
Merge pull request #77 from brainstormforce/SUR-279-sidebars
Browse files Browse the repository at this point in the history
SUR-279 Sidebars
  • Loading branch information
vrundakansara authored Oct 1, 2024
2 parents 8c7f7d9 + 909701d commit 6f1bfd5
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ module.exports = {
'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
},
globals: {
localStorage: 'readonly',
},
};
2 changes: 1 addition & 1 deletion dist/force-ui-rtl.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/force-ui.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => '7dc7b1b28db824d70a01');
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => '7a061455c0fab83c29c7');
2 changes: 1 addition & 1 deletion dist/force-ui.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/force-ui.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export { default as EditorInput } from './editor-input/index';
export { default as ProgressSteps } from './progress-steps/index';
export { default as Skeleton } from './skeleton/index';
export { default as Menu } from './menu-item/index';
export { Sidebar, SidebarHeader, SidebarBody, SidebarFooter, SidebarItem } from './sidebar/index';
export {
Breadcrumb,
BreadcrumbList,
Expand Down
3 changes: 2 additions & 1 deletion src/components/menu-item/menu-item.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const MenuList = ( {
};

return (
<div className="p-2">
<div>
<div
role="button"
tabIndex="0"
Expand All @@ -71,6 +71,7 @@ const MenuList = ( {
aria-expanded={ isOpen }
>
<span className="text-text-tertiary">{ heading }</span>

{ arrow && (
<motion.span
variants={ arrowAnimationVariants }
Expand Down
1 change: 1 addition & 0 deletions src/components/sidebar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Sidebar, SidebarHeader, SidebarBody, SidebarFooter, SidebarItem } from './sidebar.jsx';
94 changes: 94 additions & 0 deletions src/components/sidebar/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Sidebar Component Documentation

## Description

The `Sidebar` component is a flexible left panel that enhances user navigation, featuring a `Header` for titles, a `Body` for interactive elements, and a `Footer` for additional actions. Customizable with props like `className` and `screenHeight`, it adapts to various screen sizes, ensuring optimal usability on both desktop and mobile devices.

## `Sidebar` Props

### `children`
- **Type:** `ReactNode`
- **Description:** Elements to render inside the Sidebar container, typically the `SidebarHeader`, `SidebarBody`, and `SidebarFooter` components.

### `className`
- **Type:** `string`
- **Description:** Additional classes to customize the Sidebar container's styles.

### `onCollapseChange`
- **Type:** `(collapsed: boolean) => void`
- **Description:** Callback function triggered when the Sidebar's collapse state changes. Use this to handle custom logic based on whether the Sidebar is collapsed or expanded.

### `screenHeight`
- **Type:** `boolean`
- **default value:** `true`
- **Description:** Controls whether the Sidebar should take up the full screen height. If `true`, the Sidebar will have a height equal to the viewport.

### `borderOn`
- **Type:** `boolean`
- **default value:** `true`
- **Description:** Controls whether the Sidebar should have border. If `true`, the Sidebar will have a border on the right.

### `collapsible`
- **Type:** `boolean`
- **default value:** `true`
- **Description:** Controls whether the Sidebar should collapsible. If `true`, the Sidebar will have a collapse button.

## `SidebarHeader` Props

### `children`
- **Type:** `ReactNode`
- **Description:** Elements to render inside the `SidebarHeader` container, usually icons, logos, or navigation items.

## `SidebarBody` Props

### `children`
- **Type:** `ReactNode`
- **Description:** Elements to render inside the `SidebarBody` container, usually navigation links, buttons, or other interactive elements.

## `SidebarFooter` Props

### `children`
- **Type:** `ReactNode`
- **Description:** Elements to render inside the `SidebarFooter` container, typically icons, user profile, badges, or help buttons.

## `SidebarItem` Props

### `children`
- **Type:** `ReactNode`
- **Description:** Content or components to render inside the `SidebarItem`.

### `className`
- **Type:** `string`
- **Description:** Additional classes to customize the Item styling.

```jsx
<Sidebar>
<SidebarHeader>
<SidebarItem>
<Logo />
</SidebarItem>
</SidebarHeader>

<SidebarBody align="Header">
<SidebarItem>
<div className='flex gap-2'>
<div>Nav Item 1</div>
<div>Nav item 2</div>
<div>Nav Item 3</div>
</div>
</SidebarItem>
<SidebarItem>
<Button>Upgrade to Pro</Button>
</SidebarItem>
</SidebarBody>

<SidebarFooter>
<SidebarItem>
<Badge />
</SidebarItem>
<SidebarItem>
<Avatar />
</SidebarItem>
</SidebarFooter>
</Sidebar>
```
141 changes: 141 additions & 0 deletions src/components/sidebar/sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, {
createContext,
useContext,
useState,
useRef,
useEffect,
} from 'react';
import { cn } from '@/utilities/functions';
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import Tooltip from '../tooltip';
const SidebarContext = createContext();

const Sidebar = ( {
children,
className,
onCollapseChange,
collapsible = true,
screenHeight = true,
borderOn = true,
...props
} ) => {
const sideBarRef = useRef( null );
const [ isCollapsed, setIsCollapsed ] = useState( () => {
const storedState = localStorage.getItem( 'sidebar-collapsed' );
const isSmallScreen = window.innerWidth < 1280;
if ( storedState ) {
return JSON.parse( storedState );
}
return isSmallScreen;
} );

useEffect( () => {
if ( onCollapseChange ) {
onCollapseChange( isCollapsed );
}
}, [ isCollapsed, onCollapseChange ] );

useEffect( () => {
const handleScreenResize = () => {
const isSmallScreen = window.innerWidth < 1280;
if ( isSmallScreen ) {
setIsCollapsed( true );

localStorage.setItem( 'sidebar-collapsed', JSON.stringify( true ) );
} else {
const storedState = localStorage.getItem( 'sidebar-collapsed' );
setIsCollapsed( storedState ? JSON.parse( storedState ) : false );
}

if ( sideBarRef.current ) {
if ( !! screenHeight ) {
sideBarRef.current.style.height = `${ window.innerHeight }px`;
} else {
sideBarRef.current.style.height = 'auto';
}
}
};

window.addEventListener( 'resize', handleScreenResize );
handleScreenResize();

return () => {
window.removeEventListener( 'resize', handleScreenResize );
};
}, [ screenHeight ] );

return (
<SidebarContext.Provider value={ { isCollapsed, setIsCollapsed, collapsible } }>
<div
ref={ sideBarRef }
className={ cn(
'overflow-auto w-72 px-4 py-4 gap-4 flex flex-col bg-background-primary',
borderOn &&
'border-0 border-r border-solid border-border-subtle',
!! screenHeight && 'h-screen',
'transition-all duration-200',
isCollapsed && 'w-16 px-2',
className
) }
{ ...props }
>
{ children }
</div>
</SidebarContext.Provider>
);
};

const SidebarHeader = ( { children } ) => {
return <div className="space-y-2">{ children }</div>;
};

const SidebarBody = ( { children } ) => {
return <div className={ cn( 'space-y-4 grow items-start' ) }>{ children }</div>;
};

const SidebarFooter = ( { children } ) => {
const { isCollapsed, setIsCollapsed, collapsible } =
useContext( SidebarContext );
return (
<div className="space-y-4">
{ children }
{ collapsible && (
<button
className={ cn(
'bg-transparent w-full border-0 p-0 m-0 flex items-center gap-2 text-base cursor-pointer',
isCollapsed && 'justify-center'
) }
onClick={ () => {
setIsCollapsed( ! isCollapsed );

localStorage.setItem(
'sidebar-collapsed',
JSON.stringify( ! isCollapsed )
);
} }
aria-label={
isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'
}
>
{ isCollapsed ? (
<>
<Tooltip title="Expand" placement="right">
<PanelLeftOpen className="size-5" />
</Tooltip>
</>
) : (
<>
<PanelLeftClose className="size-5" /> Collapse
</>
) }
</button>
) }
</div>
);
};

const SidebarItem = ( { children, className } ) => {
return <div className={ cn( 'w-full', className ) }>{ children }</div>;
};

export { Sidebar, SidebarHeader, SidebarBody, SidebarFooter, SidebarItem };
96 changes: 96 additions & 0 deletions src/components/sidebar/sidebar.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Sidebar, SidebarHeader, SidebarBody, SidebarFooter, SidebarItem } from './sidebar';
import Button from '../button';

export default {
title: 'Organism/Sidebar',
component: Sidebar,
parameters: {
layout: 'left',
},
tags: [ 'autodocs' ],
argTypes: {
children: {
description: 'Content to render inside the Sidebar. This typically includes `SidebarHeader`, `SidebarBody`, and `SidebarFooter` components.',
control: { type: 'none' },
},
className: {
description: 'Optional custom CSS classes to apply to the Sidebar container for styling.',
control: { type: 'text' },
table: {
type: { summary: 'string' },
},
},
borderOn: {
description: 'Controls whether a border should appear on the right of the Sidebar.',
control: { type: 'boolean' },
defaultValue: true,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: true },
},
},
collapsible: {
description: 'Determines if the Sidebar can be collapsed or not. If `true`, a collapse button is shown.',
control: { type: 'boolean' },
defaultValue: true,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: true },
},
},
screenHeight: {
description: 'Determines whether the Sidebar should occupy the full screen height.',
control: { type: 'boolean' },
defaultValue: true,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: true },
},
},
onCollapseChange: {
description: 'Callback function triggered when the Sidebar collapse state changes. Use this to handle logic based on collapse/expand states.',
action: 'onCollapseChange',
},
},
};

const Template = ( args ) => (
<Sidebar { ...args }
>
<SidebarHeader>
<SidebarItem>
<img
width="240px"
alt="Logo"
src="https://upload.wikimedia.org/wikipedia/commons/4/44/Hyundai_Motor_Company_logo.svg"
/>
</SidebarItem>
</SidebarHeader>
<SidebarBody>
<SidebarItem>
<div className="flex flex-col gap-2">
{ [ 1, 2, 3, 4, 5, 6 ].map( ( num ) => (
<div key={ num }>Nav Item</div>
) ) }
</div>

</SidebarItem>

</SidebarBody>
<SidebarFooter>
<Button className="w-full">
Pro
</Button>
</SidebarFooter>
</Sidebar>
);

export const DefaultSidebar = Template.bind( {} );
DefaultSidebar.args = {
screenHeight: true,
borderOn: true,
collapsible: true,
};

DefaultSidebar.storyName = 'Sidebar';

0 comments on commit 6f1bfd5

Please sign in to comment.