Skip to content

Commit

Permalink
Merge pull request #58 from WestpacGEL/feature/collapsible
Browse files Browse the repository at this point in the history
Feature/collapsible
  • Loading branch information
samithaf authored Sep 4, 2023
2 parents 770cd34 + 63d3927 commit e9c225e
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 0 deletions.
1 change: 1 addition & 0 deletions apps/docs/content/components/collapsible.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: Collapsible
61 changes: 61 additions & 0 deletions apps/docs/content/components/collapsible/code.mdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Collapsible sizes

```jsx
<div>
<div className="mb-2">
<h2 className="typography-body-10 mb-1">Small</h2>

<Collapsible text="Toggle collapsible" size="small">
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>
</div>
<hr className="mb-2"/>
<div className="mb-2">
<h2 className="typography-body-10 mb-1">Medium (default)</h2>

<Collapsible text="Toggle collapsible" size="medium">
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>
</div>
<hr className="mb-2"/>
<div className="mb-2">
<h2 className="typography-body-10 mb-1">Large</h2>

<Collapsible text="Toggle collapsible" size="large">
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>
</div>
<hr className="mb-2"/>
<div className="mb-2">
<h2 className="typography-body-10 mb-1">XLarge</h2>

<Collapsible text="Toggle collapsible" size="xlarge">
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>
</div>
</div>
```
1 change: 1 addition & 0 deletions apps/docs/src/components/code/code.inject-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
ProgressBar,
Link,
Table,
Collapsible,
DatePicker,
} from '@westpac/ui';

Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/components/layout/nav-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const DEFAULT_NAV_ITEMS: NavItem[] = [
{ href: '/components/progress-bar', label: 'Progress Bar' },
{ href: '/components/link', label: 'Link' },
{ href: '/components/table', label: 'Table' },
{ href: '/components/collapsible', label: 'Collapsible' },
{ href: '/components/date-picker', label: 'Date Picker' },
],
},
Expand Down
66 changes: 66 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { AnimatePresence, LazyMotion, m } from 'framer-motion';
import React, { useCallback, useState } from 'react';

import { Button } from '../button/index.js';
import { ExpandLessIcon, ExpandMoreIcon, IconProps } from '../icon/index.js';

import { styles as collapsibleStyles } from './collapsible.styles.js';
import { type CollapsibleProps } from './collapsible.types.js';

const loadAnimations = () => import('./collapsible.utils.js').then(res => res.default);

export function Collapsible({
className,
children,
open = false,
text,
size = 'medium',
onClick = () => undefined,
}: CollapsibleProps) {
const [contentOpen, setContentOpen] = useState(open);

const ButtonIcon = (props: IconProps) => {
if (!contentOpen) return <ExpandMoreIcon color="link" {...props} />;
return <ExpandLessIcon color="link" {...props} />;
};

const handleClick = useCallback(() => {
onClick();
setContentOpen(contentOpen => !contentOpen);
}, [contentOpen]);

const styles = collapsibleStyles({ open: contentOpen });

return (
<>
<Button
className={styles.base({ className })}
look="link"
iconAfter={ButtonIcon}
onClick={handleClick}
size={size}
aria-expanded={contentOpen}
aria-controls="expanded-content"
>
{text}
</Button>
<LazyMotion features={loadAnimations}>
<AnimatePresence>
{contentOpen && (
<m.div
key="content"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className={styles.content()}
id="expanded-content"
aria-hidden={!contentOpen}
>
{children}
</m.div>
)}
</AnimatePresence>
</LazyMotion>
</>
);
}
44 changes: 44 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { Collapsible } from './collapsible.component.js';

describe('Collapsible', () => {
it('renders the component', () => {
const { container } = render(<Collapsible text="test button">Test Text</Collapsible>);
expect(container).toBeInTheDocument();
});

it('should show the content as default if open prop is possed', () => {
const { getByText } = render(
<Collapsible text="test button" open>
Test Text
</Collapsible>,
);
expect(getByText('Test Text')).toBeInTheDocument();
});

it('should show the content when the button is clicked', async () => {
const user = userEvent.setup();
const { getByText, getByRole } = render(<Collapsible text="test button">Test Text</Collapsible>);
expect(screen.queryByText('Test Text')).not.toBeInTheDocument();
await act(() => {
user.click(getByRole('button', { name: 'test button Expand More' }));
});
await waitFor(() => expect(getByText('Test Text')).toBeInTheDocument());
});

it('should call onClick when custom onClick passed as prop', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
const { getByRole } = render(
<Collapsible text="test button" onClick={onClick}>
Test Text
</Collapsible>,
);
await act(() => {
user.click(getByRole('button', { name: 'test button Expand More' }));
});
await waitFor(() => expect(onClick).toBeCalled());
});
});
103 changes: 103 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { type Meta, StoryFn, type StoryObj } from '@storybook/react';

import { Collapsible } from './collapsible.component.js';

const meta: Meta<typeof Collapsible> = {
title: 'Example/Collapsible',
component: Collapsible,
tags: ['autodocs'],
decorators: [
(Story: StoryFn) => (
<div className="typography-body-10 p-3">
<Story />
</div>
),
],
parameters: {
layout: 'center',
},
};

export default meta;
type Story = StoryObj<typeof meta>;

const SIZES = ['small', 'medium', 'large', 'xlarge'];

/**
* > Default usage example
*/
export const DefaultStory: Story = {
args: {
text: 'Toggle collapsible',
children:
'Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta nobis modi officia incidunt eos dolores atque, unde error delectus officiis.',
},
};

/**
* > Examples of sizes of collapsible button
*/
export const CollapsibleSizes = () => (
<div className="typography-body-10 p-3">
{SIZES.map((size: any) => (
<div className="mb-2" key={size}>
<h2 className="typography-body-8 mb-1 font-bold">{size}</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur illum quidem neque quod impedit,
praesentium maiores unde perspiciatis accusantium non, quae debitis ut aliquid numquam ipsa hic tempora
deleniti deserunt!
</p>

<Collapsible text="Toggle collapsible" size={size}>
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>

<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur illum quidem neque quod impedit,
praesentium maiores unde perspiciatis accusantium non, quae debitis ut aliquid numquam ipsa hic tempora
deleniti deserunt!
</p>
</div>
))}
</div>
);

/**
* > Setting the default of collapsible to open
*/
export const CollapsibleOpen = () => (
<div className="typography-body-10 p-3">
{SIZES.map((size: any) => (
<div className="mb-2" key={size}>
<h2 className="typography-body-8 mb-1 font-bold">{size}</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur illum quidem neque quod impedit,
praesentium maiores unde perspiciatis accusantium non, quae debitis ut aliquid numquam ipsa hic tempora
deleniti deserunt!
</p>

<Collapsible text="Toggle collapsible" size={size} open>
<p>
Hello vivamus sagittis lacus vel augue laoreet rutrum faucibus. Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Nesciunt laboriosam, mollitia magnam ad magni consequuntur hic et quos optio corrupti
praesentium veniam aspernatur minima aperiam ut quas, possimus non architecto. Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Aut animi velit in? Suscipit nostrum itaque voluptatibus dolorem qui soluta
nobis modi officia incidunt eos dolores atque, unde error delectus officiis.
</p>
</Collapsible>

<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur illum quidem neque quod impedit,
praesentium maiores unde perspiciatis accusantium non, quae debitis ut aliquid numquam ipsa hic tempora
deleniti deserunt!
</p>
</div>
))}
</div>
);
16 changes: 16 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { tv } from 'tailwind-variants';

export const styles = tv(
{
slots: {
base: 'text-text focus:focus-outline px-0 no-underline hover:underline',
content: 'typography-body-10 mb-2 block',
},
variants: {
open: {
true: { base: 'block' },
},
},
},
{ responsiveVariants: ['xsl', 'sm', 'md', 'lg', 'xl'] },
);
19 changes: 19 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HTMLAttributes } from 'react';

import { ButtonProps } from '../button/index.js';

export type CollapsibleProps = {
/**
* A function for the onClick event
*/
onClick?: () => void;
/**
* State of whether the Collapsible is open
*/
open?: boolean;
/**
* Button text
*/
text: string;
} & Pick<ButtonProps, 'size'> &
HTMLAttributes<Element>;
3 changes: 3 additions & 0 deletions packages/ui/src/components/collapsible/collapsible.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { domAnimation } from 'framer-motion';

export default domAnimation;
2 changes: 2 additions & 0 deletions packages/ui/src/components/collapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Collapsible } from './collapsible.component.js';
export { type CollapsibleProps } from './collapsible.types.js';
1 change: 1 addition & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './link/index.js';
export * from './date-picker/index.js';
export * from './grid/index.js';
export * from './table/index.js';
export * from './collapsible/index.js';

0 comments on commit e9c225e

Please sign in to comment.