Skip to content

Commit

Permalink
feat: drawer footer standard props and some improvements (#532)
Browse files Browse the repository at this point in the history
* feat(drawer): standardized drawer footer props

* remove: button box shadow

* add: drawer close button; import styles from css module

* (wip): use decorator to display open drawer button

* fix: drawer props now visible from Show Code panel

* move types to dedicated file

* refactor: drawer mocks

* add: custom footer test

* apply suggestions

* remove: changes to button

* fix: empty footer rendering

* Update src/components/Drawer/Drawer.mocks.tsx

* fix indentation

* add: doc link prop
  • Loading branch information
GiovannaMonti authored Jul 12, 2024
1 parent 916fad5 commit 1093548
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 61 deletions.
32 changes: 26 additions & 6 deletions src/components/Drawer/Drawer.Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,34 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { ReactElement, ReactNode } from 'react'
import { ReactNode } from 'react'
import { isEmpty } from 'lodash-es'

export type DrawerFooter = ReactNode
import { CustomDrawerFooter, DrawerFooter, FooterProps } from './Drawer.types'
import styles from './Drawer.module.css'

export type FooterProps = {
footer: DrawerFooter,
function isDrawerFooter(obj: DrawerFooter | CustomDrawerFooter): obj is DrawerFooter {
return (
obj
&& ('buttons' in obj || 'extra' in obj)
)
}

export const Footer = ({ footer }: FooterProps): ReactElement => {
return <footer>{footer}</footer>
export const Footer = ({ footer }: FooterProps): ReactNode => {
if (!footer || isEmpty(footer)) {
return null
}

if (isDrawerFooter(footer)) {
const { buttons, extra } = footer
return <footer className={styles.footer}>
<div className={styles.extra}>{extra}</div>
<div className={styles.footerButtons}>
{buttons}
</div>
</footer>
}

return footer
}

30 changes: 23 additions & 7 deletions src/components/Drawer/Drawer.Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,33 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { ReactElement, ReactNode, useMemo } from 'react'
import { ReactElement, useCallback, useMemo } from 'react'

import { Button } from '../Button'
import { H4 } from '../Typography/HX/H4'
import { Icon } from '../Icon'
import { TitleProps } from './Drawer.types'
import { useTheme } from '../../hooks/useTheme'

export type DrawerTitle = ReactNode
export const Title = ({ docLink, title }: TitleProps): ReactElement => {
const { palette } = useTheme()

export type TitleProps = {
title: DrawerTitle,
}
const docLinkIcon = useMemo(() => (
<Icon color={palette?.action?.link?.active} name="PiBookOpen" size={16} />
), [palette?.action?.link?.active])

const onClickDocLink = useCallback(() => window.open(docLink, '_blank'), [docLink])

export const Title = ({ title }: TitleProps): ReactElement => {
const ellipsis = useMemo(() => ({ rows: 1, tooltip: title }), [title])
return <H4 ellipsis={ellipsis}>{title}</H4>
return <>
<H4 ellipsis={ellipsis}>{title}</H4>
{docLink && <div>
<Button
icon={docLinkIcon}
shape={Button.Shape.Circle}
type={Button.Type.Ghost}
onClick={onClickDocLink}
/>
</div>}
</>
}
37 changes: 12 additions & 25 deletions src/components/Drawer/Drawer.mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
import { ReactElement } from 'react'

import { Button } from '../Button'
import { Drawer } from './Drawer'
import { DrawerProps } from './Drawer.props'
import { useDrawer } from '../../hooks/useDrawer'
import { Hierarchy } from '../Button/Button.types'

export const DrawerLipsum = (): ReactElement => {
return (
Expand All @@ -40,33 +38,22 @@ export const DrawerLipsum = (): ReactElement => {
)
}

export const DrawerLipumTitle = (): ReactElement => {
export const DrawerLipsumTitle = (): ReactElement => {
return <span>{'Drawer Lipsum'}</span>
}

export const DrawerLipsumFooter = ({ closeDrawer }: {closeDrawer: () => void}): ReactElement => {
export const DrawerLipsumFooterButton = ({ text, hierarchy } : {text: string, hierarchy?: Hierarchy}): ReactElement => {
return (
<div>
<Button onClick={closeDrawer}>
{'Close'}
</Button>
</div>
<Button hierarchy={hierarchy}>
{text}
</Button>
)
}

export const WithOpenButton = (props: DrawerProps): ReactElement => {
const { isVisible, openDrawer, closeDrawer } = useDrawer()
return (
<>
<Button onClick={openDrawer}>Open Drawer</Button>
<Drawer
{...props}
footer={<DrawerLipsumFooter closeDrawer={closeDrawer} />}
isVisible={isVisible}
onClose={closeDrawer}
>
<DrawerLipsum />
</Drawer>
</>
)
export const drawerLipsumFooter = {
buttons: [
<DrawerLipsumFooterButton key="action-button" text="Primary Action" />,
<DrawerLipsumFooterButton hierarchy={Hierarchy.Neutral} key="secondary-action-button" text="Secondary Action" />,
],
extra: 'Extra text',
}
48 changes: 48 additions & 0 deletions src/components/Drawer/Drawer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.drawer {

:global(.mia-platform-drawer-header-title) {
flex-direction: row-reverse;

:global(.mia-platform-drawer-close) {
margin: 0;
padding: 0;
}

:global(.mia-platform-drawer-title) {
display: flex;
align-items: center;
gap: var(--spacing-gap-sm, 8px);

h4 {
margin: 0;
}
}
}

:global(.mia-platform-drawer-footer) {
padding: var(--spacing-padding-lg, 16px) var(--spacing-padding-xl, 24px);
min-height: var(--shape-size-xl, 32px);
}

.footer {
display: flex;
align-items: center;
justify-content: end;
gap: var(--spacing-gap-sm, 8px);
}

.footerButtons {
margin-inline-start: 0;
display: flex;
flex-direction: row-reverse;
gap: var(--spacing-gap-sm, 8px);

:global(*) {
margin: 0 !important;
}
}

.extra {
flex: 1;
}
}
10 changes: 8 additions & 2 deletions src/components/Drawer/Drawer.props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { ReactNode } from 'react'

import { DrawerTitle } from './Drawer.Title'
import { CustomDrawerFooter, DrawerFooter, DrawerTitle } from './Drawer.types'

export type DrawerProps = {

Expand All @@ -32,10 +32,16 @@ export type DrawerProps = {
*/
destroyOnClose?: boolean,

/**
* The reference url for documentation of the drawer contents.
* If present, a button is shown next to the title that, when clicked, opens the url in a new tab.
*/
docLink?: string,

/**
* Drawer footer.
*/
footer?: ReactNode,
footer?: DrawerFooter | CustomDrawerFooter,

/**
* drawer id for DOM node.
Expand Down
56 changes: 52 additions & 4 deletions src/components/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,73 @@
*/

import type { Meta, StoryObj } from '@storybook/react'
import { useMemo, useState } from 'react'

import { DrawerLipumTitle, WithOpenButton } from './Drawer.mocks'
import { DrawerLipsum, DrawerLipsumTitle, drawerLipsumFooter } from './Drawer.mocks'
import { Button } from '../Button'
import { Drawer } from '.'

const defaults = {
title: <DrawerLipumTitle />,
title: <DrawerLipsumTitle />,
}

const meta = {
component: Drawer,
args: defaults,
argTypes: {
children: { control: false },
isVisible: { control: false },
},
decorators: [
(Story, context) => {
const [isVisible, setIsVisible] = useState(false)

const customContext = useMemo(() => ({
...context,
args: {
...context.args,
isVisible,
onClose: () => setIsVisible(false),
},
}), [context, isVisible])

return <div>
<Button
onClick={() => setIsVisible(true)}
>
Open drawer
</Button>
<Story {...customContext} />
</div>
},
],
render: (_, { args }) => <Drawer {...args}>
<DrawerLipsum />
</Drawer>,
} satisfies Meta<typeof Drawer>

export default meta
type Story = StoryObj<typeof meta>

export const BasicExample: Story = {
decorators: [(_, { args }) => <WithOpenButton {...args} />],
export const BasicExample: Story = {}

export const WithDocLink: Story = {
args: {
...meta.args,
docLink: 'https://www.google.com/',
},
}

export const WithStandardFooterProps: Story = {
args: {
...meta.args,
footer: drawerLipsumFooter,
},
}

export const WithCustomFooter: Story = {
args: {
...meta.args,
footer: <div>{'Custom footer content'}</div>,
},
}
35 changes: 31 additions & 4 deletions src/components/Drawer/Drawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,59 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { DrawerLipsum, DrawerLipsumFooter, DrawerLipumTitle } from './Drawer.mocks'
import { DrawerLipsum, DrawerLipsumFooterButton, DrawerLipsumTitle } from './Drawer.mocks'
import { render, screen } from '../../test-utils'
import { Drawer } from './Drawer'
import { DrawerProps } from './Drawer.props'

describe('Drawer', () => {
const props: DrawerProps = {
children: 'Drawer Content',
footer: <DrawerLipsumFooter closeDrawer={jest.fn()} />,
footer: {
buttons: [<DrawerLipsumFooterButton key="close-drawer" text="Close drawer" />],
extra: 'extra content',
},
isVisible: true,
title: <DrawerLipumTitle />,
title: <DrawerLipsumTitle />,
onClose: jest.fn(),
}

beforeEach(() => jest.resetAllMocks())

it('renders drawer with doc link', () => {
const customProps = {
...props,
docLink: 'https://www.google.com/',
}
render(<Drawer {...customProps} ><DrawerLipsum /></Drawer>)

expect(screen.getByText(/drawer lipsum/i)).toBeVisible()
expect(screen.getByRole('button', { name: /pibookopen/i })).toBeVisible()
})

it('renders drawer with provided title and footer', () => {
const { baseElement } = render(<Drawer {...props} ><DrawerLipsum /></Drawer>)

expect(screen.getByText(/drawer lipsum/i)).toBeVisible()
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /close drawer/i })).toBeInTheDocument()
expect(screen.getByText(/extra content/i)).toBeInTheDocument()
expect(screen.getByText(/Lorem ipsum dolor sit amet,/i)).toBeInTheDocument()

expect(baseElement).toMatchSnapshot()
})

it('renders drawer with custom footer', () => {
const customProps = {
...props,
footer: <div>Custom footer content</div>,
}
render(<Drawer {...customProps} ><DrawerLipsum /></Drawer>)

expect(screen.getByText(/drawer lipsum/i)).toBeVisible()
expect(screen.getByText(/custom footer content/i)).toBeInTheDocument()
expect(screen.getByText(/Lorem ipsum dolor sit amet,/i)).toBeInTheDocument()
})

it('does not render drawer when isVisible is false', () => {
render(<Drawer {...props} isVisible={false} >{'the-content'}</Drawer>)
expect(screen.queryByText(/drawer lipsum/i)).toBeNull()
Expand Down
16 changes: 8 additions & 8 deletions src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ import { ReactElement } from 'react'

import { DrawerProps } from './Drawer.props'
import { Footer } from './Drawer.Footer'
import { Icon } from '../Icon'
import { Title } from './Drawer.Title'

const styles = {
footer: { padding: '24px' },
}
import styles from './Drawer.module.css'

const DRAWER_WIDTH = 512
const closeIcon = <Icon color="currentColor" name="PiX" size={16} />

export const Drawer = ({
children,
destroyOnClose,
docLink,
footer,
id,
isVisible,
Expand All @@ -41,14 +41,14 @@ export const Drawer = ({
}: DrawerProps): ReactElement => {
return (
<AntdDrawer
closeIcon={null}
className={styles.drawer}
closeIcon={closeIcon}
destroyOnClose={destroyOnClose}
footer={<Drawer.Footer footer={footer} />}
footer={footer && <Drawer.Footer footer={footer} />}
id={id}
key={key}
open={isVisible}
styles={styles}
title={<Drawer.Title title={title} />}
title={<Drawer.Title docLink={docLink} title={title} />}
width={DRAWER_WIDTH}
onClose={onClose}
>
Expand Down
Loading

0 comments on commit 1093548

Please sign in to comment.