Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: drawer footer standard props and some improvements #532

Merged
merged 14 commits into from
Jul 12, 2024
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
Loading