Skip to content

Commit

Permalink
feat(sidebar): add sidebar collapse button (#3160)
Browse files Browse the repository at this point in the history
* feat(sidebar): add collapse button

* chore: changeset

* chore: pr comments

* chore: pr fixes 2

---------

Co-authored-by: TheSisb <shadiisber@gmail.com>
Co-authored-by: Shadi <TheSisb@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 20, 2023
1 parent 80c9f85 commit d1d3088
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 82 deletions.
6 changes: 6 additions & 0 deletions .changeset/cold-teachers-bow.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/sidebar': minor
'@twilio-paste/core': minor
---

[Sidebar] Add `SidebarCollapseButton` and `SidebarCollapseButtonWrapper` components. Renamed ContentWrappers
7 changes: 5 additions & 2 deletions packages/paste-codemods/tools/.cache/mappings.json
Expand Up @@ -184,9 +184,12 @@
"SideModalHeader": "@twilio-paste/core/side-modal",
"SideModalHeading": "@twilio-paste/core/side-modal",
"useSideModalState": "@twilio-paste/core/side-modal",
"OverlaySidebarContentWrapper": "@twilio-paste/core/sidebar",
"PushSidebarContentWrapper": "@twilio-paste/core/sidebar",
"Sidebar": "@twilio-paste/core/sidebar",
"SidebarCollapseButton": "@twilio-paste/core/sidebar",
"SidebarCollapseButtonWrapper": "@twilio-paste/core/sidebar",
"SidebarContext": "@twilio-paste/core/sidebar",
"SidebarOverlayContentWrapper": "@twilio-paste/core/sidebar",
"SidebarPushContentWrapper": "@twilio-paste/core/sidebar",
"SkeletonLoader": "@twilio-paste/core/skeleton-loader",
"Spinner": "@twilio-paste/core/spinner",
"Switch": "@twilio-paste/core/switch",
Expand Down
119 changes: 97 additions & 22 deletions packages/paste-core/components/sidebar/__tests__/index.spec.tsx
@@ -1,21 +1,36 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
import {Theme} from '@twilio-paste/theme';
import {Box} from '@twilio-paste/box';
import {CustomizationProvider} from '@twilio-paste/customization';

import {Sidebar, PushSidebarContentWrapper, OverlaySidebarContentWrapper} from '../src';
import type {Variants} from '../src';
import {
Sidebar,
SidebarCollapseButton,
SidebarCollapseButtonWrapper,
SidebarPushContentWrapper,
SidebarOverlayContentWrapper,
} from '../src';
import type {SidebarProps} from '../src';

const MockPushSidebar = ({collapsed, variant = 'default'}: {collapsed?: boolean; variant?: Variants}): JSX.Element => {
const MockPushSidebar = ({
collapsed,
variant = 'default',
}: {
collapsed?: boolean;
variant?: SidebarProps['variant'];
}): JSX.Element => {
return (
<Theme.Provider theme="twilio">
<Sidebar aria-label="main" collapsed={collapsed} variant={variant}>
<Box color="colorTextInverse">Sidebar header</Box>
<SidebarCollapseButtonWrapper>
<SidebarCollapseButton i18nCollapseLabel="Close sidebar" i18nExpandLabel="Open sidebar" />
</SidebarCollapseButtonWrapper>
</Sidebar>
<PushSidebarContentWrapper collapsed={collapsed} variant={variant}>
<SidebarPushContentWrapper collapsed={collapsed} variant={variant}>
<div>Content area</div>
</PushSidebarContentWrapper>
</SidebarPushContentWrapper>
</Theme.Provider>
);
};
Expand All @@ -25,16 +40,19 @@ const MockOverlaySidebar = ({
variant = 'default',
}: {
collapsed?: boolean;
variant?: Variants;
variant?: SidebarProps['variant'];
}): JSX.Element => {
return (
<Theme.Provider theme="twilio">
<Sidebar aria-label="main" collapsed={collapsed} variant={variant}>
<Box color="colorTextInverse">Sidebar header</Box>
<SidebarCollapseButtonWrapper>
<SidebarCollapseButton i18nCollapseLabel="Close sidebar" i18nExpandLabel="Open sidebar" />
</SidebarCollapseButtonWrapper>
</Sidebar>
<OverlaySidebarContentWrapper collapsed={collapsed} variant={variant}>
<SidebarOverlayContentWrapper collapsed={collapsed} variant={variant}>
<div>Content area</div>
</OverlaySidebarContentWrapper>
</SidebarOverlayContentWrapper>
</Theme.Provider>
);
};
Expand All @@ -44,22 +62,22 @@ describe('Sidebar', () => {
* PUSH
*/
describe('Push Sidebar', () => {
it('should render collapsed', () => {
it('should have an id', () => {
render(<MockPushSidebar collapsed />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('false');
expect(nav).toHaveAttribute('id');
});

it('should render expanded', () => {
render(<MockPushSidebar collapsed={false} />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('true');
expect(nav.style.width).toBe('15rem');
});

it('should render expanded by default', () => {
render(<MockPushSidebar />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('true');
expect(nav.style.width).toBe('15rem');
});

it('should render compact width', () => {
Expand All @@ -73,22 +91,22 @@ describe('Sidebar', () => {
* OVERLAY
*/
describe('Overlay Sidebar', () => {
it('should render collapsed', () => {
it('should have an id', () => {
render(<MockOverlaySidebar collapsed />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('false');
expect(nav).toHaveAttribute('id');
});

it('should render expanded', () => {
render(<MockOverlaySidebar collapsed={false} />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('true');
expect(nav.style.width).toBe('15rem');
});

it('should render expanded by default', () => {
render(<MockOverlaySidebar />);
const nav = screen.getByRole('navigation');
expect(nav.getAttribute('aria-expanded')).toEqual('true');
expect(nav.style.width).toBe('15rem');
});

it('should render compact width', () => {
Expand All @@ -98,6 +116,29 @@ describe('Sidebar', () => {
});
});

/**
* SIDEBAR COLLAPSE BUTTON
*/
describe('Sidebar Collapse Button', () => {
it('should have aria-expanded and aria-controls set correctly when collapsed', async () => {
render(<MockOverlaySidebar collapsed />);
const toggleButton = screen.getByRole('button');
const nav = screen.getByRole('navigation');
expect(toggleButton.getAttribute('aria-controls')).toEqual(nav.getAttribute('id'));
expect(toggleButton.getAttribute('aria-expanded')).toEqual('false');
expect(toggleButton.textContent).toBe('Open sidebar');
});

it('should have aria-expanded and aria-controls set correctly when expanded', async () => {
render(<MockOverlaySidebar collapsed={false} />);
const toggleButton = screen.getByRole('button');
const nav = screen.getByRole('navigation');
expect(toggleButton.getAttribute('aria-controls')).toEqual(nav.getAttribute('id'));
expect(toggleButton.getAttribute('aria-expanded')).toEqual('true');
expect(toggleButton.textContent).toBe('Close sidebar');
});
});

/**
* Customization
*/
Expand All @@ -109,23 +150,38 @@ describe('Sidebar', () => {
theme={TestTheme}
elements={{
SIDEBAR: {backgroundColor: 'colorBackgroundPrimary', margin: 'space50'},
PUSH_SIDEBAR_CONTENT_WRAPPER: {backgroundColor: 'colorBackgroundPrimary', margin: 'space50'},
SIDEBAR_COLLAPSE_BUTTON: {
padding: 'space40',
},
SIDEBAR_COLLAPSE_BUTTON_WRAPPER: {
padding: 'space40',
},
SIDEBAR_PUSH_CONTENT_WRAPPER: {backgroundColor: 'colorBackgroundPrimary', margin: 'space50'},
}}
>
<Sidebar aria-label="main" variant="compact" data-testid="aaa">
<Box color="colorTextInverse">Sidebar header</Box>
<SidebarCollapseButtonWrapper data-testid="collapseButtonWrapper">
<SidebarCollapseButton i18nCollapseLabel="Close sidebar" i18nExpandLabel="Open sidebar" />
</SidebarCollapseButtonWrapper>
</Sidebar>

{/* Must wrap content area */}
<PushSidebarContentWrapper variant="compact" data-testid="contentwrapper">
<SidebarPushContentWrapper variant="compact" data-testid="contentwrapper">
<div>Content area</div>
</PushSidebarContentWrapper>
</SidebarPushContentWrapper>
</CustomizationProvider>
);
const nav = screen.getByRole('navigation');
expect(nav).toHaveStyleRule('margin', '1rem');
expect(nav).toHaveStyleRule('background-color', 'rgb(2, 99, 224)');

const sidebarButton = screen.getByRole('button');
expect(sidebarButton).toHaveStyleRule('padding', '0.75rem');

const sidebarButtonWrapper = screen.getByTestId('collapseButtonWrapper');
expect(sidebarButtonWrapper).toHaveStyleRule('padding', '0.75rem');

const contentWrapper = screen.getByTestId('contentwrapper');
expect(contentWrapper).toHaveStyleRule('margin', '1rem');
expect(contentWrapper).toHaveStyleRule('background-color', 'rgb(2, 99, 224)');
Expand All @@ -139,22 +195,41 @@ describe('Sidebar', () => {
elements={{
XSIDE: {backgroundColor: 'colorBackgroundPrimary', margin: 'space50'},
XSIDE_WRAPPER: {backgroundColor: 'colorBackgroundPrimary', margin: 'space50'},
XSIDE_COLLAPSE_BUTTON: {
padding: 'space40',
},
XSIDE_COLLAPSE_BUTTON_WRAPPER: {
padding: 'space40',
},
}}
>
<Sidebar aria-label="main" variant="compact" element="XSIDE">
<Box color="colorTextInverse">Sidebar header</Box>
<SidebarCollapseButtonWrapper element="XSIDE_COLLAPSE_BUTTON_WRAPPER" data-testid="collapseButtonWrapper">
<SidebarCollapseButton
element="XSIDE_COLLAPSE_BUTTON"
i18nCollapseLabel="Close sidebar"
i18nExpandLabel="Open sidebar"
/>
</SidebarCollapseButtonWrapper>
</Sidebar>

{/* Must wrap content area */}
<PushSidebarContentWrapper variant="compact" element="XSIDE_WRAPPER" data-testid="contentwrapper">
<SidebarPushContentWrapper variant="compact" element="XSIDE_WRAPPER" data-testid="contentwrapper">
<div>Content area</div>
</PushSidebarContentWrapper>
</SidebarPushContentWrapper>
</CustomizationProvider>
);
const nav = screen.getByRole('navigation');
expect(nav).toHaveStyleRule('margin', '1rem');
expect(nav).toHaveStyleRule('background-color', 'rgb(2, 99, 224)');

const sidebarButton = screen.getByRole('button');
expect(sidebarButton).toHaveStyleRule('padding', '0.75rem');

const sidebarButtonWrapper = screen.getByTestId('collapseButtonWrapper');
expect(sidebarButtonWrapper).toHaveStyleRule('padding', '0.75rem');

const contentWrapper = screen.getByTestId('contentwrapper');
expect(contentWrapper).toHaveStyleRule('margin', '1rem');
expect(contentWrapper).toHaveStyleRule('background-color', 'rgb(2, 99, 224)');
Expand Down
14 changes: 14 additions & 0 deletions packages/paste-core/components/sidebar/package.json
Expand Up @@ -24,15 +24,22 @@
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.0",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.0",
"@twilio-paste/icons": "^11.0.0",
"@twilio-paste/screen-reader-only": "^12.0.0",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
Expand All @@ -41,15 +48,22 @@
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
},
"devDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.0",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.2",
"@twilio-paste/icons": "^11.0.0",
"@twilio-paste/screen-reader-only": "^12.0.0",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
Expand Down
28 changes: 16 additions & 12 deletions packages/paste-core/components/sidebar/src/Sidebar.tsx
Expand Up @@ -4,7 +4,9 @@ import type {BoxProps} from '@twilio-paste/box';
import {useSpring, animated} from '@twilio-paste/animation-library';
import {useTheme} from '@twilio-paste/theme';
import {useWindowSize} from '@twilio-paste/utils';
import {useUID} from '@twilio-paste/uid-library';

import {SidebarContext} from './SidebarContext';
import type {Variants} from './types';

const StyledSidebar = React.forwardRef<HTMLDivElement, BoxProps>((props, ref) => (
Expand All @@ -23,7 +25,6 @@ const StyledSidebar = React.forwardRef<HTMLDivElement, BoxProps>((props, ref) =>
/>
));
StyledSidebar.displayName = 'StyledSidebar';

const AnimatedStyledSidebar = animated(StyledSidebar);

const config = {
Expand Down Expand Up @@ -67,6 +68,7 @@ export interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {

export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
({collapsed = false, variant = 'default', element = 'SIDEBAR', children, ...props}, ref) => {
const sidebarId = useUID();
const {breakpointIndex} = useWindowSize();
const theme = useTheme();

Expand All @@ -85,17 +87,19 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
const styles = useSpring(springConfig);

return (
<AnimatedStyledSidebar
{...safelySpreadBoxProps(props)}
ref={ref}
element={element}
width={['100%', isCompact && collapsed ? 'sizeSidebarCompact' : 'sizeSidebar']}
style={styles}
aria-label={props['aria-label']}
aria-expanded={!collapsed}
>
{children}
</AnimatedStyledSidebar>
<SidebarContext.Provider value={{collapsed, variant, sidebarId}}>
<AnimatedStyledSidebar
{...safelySpreadBoxProps(props)}
ref={ref}
element={element}
width={['100%', isCompact && collapsed ? 'sizeSidebarCompact' : 'sizeSidebar']}
style={styles}
aria-label={props['aria-label']}
id={sidebarId}
>
{children}
</AnimatedStyledSidebar>
</SidebarContext.Provider>
);
}
);
Expand Down

0 comments on commit d1d3088

Please sign in to comment.