Skip to content

Commit

Permalink
feat(ContextMenu): Create base component
Browse files Browse the repository at this point in the history
  • Loading branch information
m7kvqbe1 committed Oct 5, 2020
1 parent 1dd9b3a commit c8a32e0
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import { Meta } from '@storybook/react/types-6-0'

import { IconEdit, IconDelete, IconAdd } from '@royalnavy/icon-library'

import { ContextMenu, ContextMenuItem, ContextMenuDivider } from '.'
import { Link } from '../Link'

export default { component: ContextMenu, title: 'ContextMenu' } as Meta

export const Default = () => (
<ContextMenu>
<ContextMenuItem
icon={<IconEdit />}
link={<Link href="/edit">Edit</Link>}
/>
<ContextMenuItem
icon={<IconDelete />}
link={<Link href="/delete">Delete</Link>}
/>
<ContextMenuItem link={<Link href="/delete">Action</Link>} />
<ContextMenuDivider />
<ContextMenuItem icon={<IconAdd />} link={<Link href="/add">Add</Link>} />
<ContextMenuDivider />
<ContextMenuItem
link={<Link href="/something-else">Do something else</Link>}
/>
<ContextMenuDivider />
<ContextMenuItem
link={(
<Link href="/something-else">
This is too much text to put into a context menu item
</Link>
)}
/>
</ContextMenu>
)

Default.storyName = 'Default'
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { RenderResult, render } from '@testing-library/react'
import { IconSettings } from '@royalnavy/icon-library'

import { ContextMenu, ContextMenuItem, ContextMenuDivider } from '.'
import { Link } from '../Link'

const CustomLink = ({ children, onClick }: any) => {
return (
<button onClick={onClick} data-testid="context-menu-custom-link">
{children}
</button>
)
}

describe('ContextMenu', () => {
let wrapper: RenderResult
let onClickSpy: (e: React.MouseEvent<HTMLElement>) => void

describe('With link', () => {
beforeEach(() => {
wrapper = render(
<ContextMenu>
<ContextMenuItem link={<Link href="/hello-foo">Hello, Foo!</Link>} />
<ContextMenuItem link={<Link href="/hello-bar">Hello, Bar!</Link>} />
</ContextMenu>
)
})

it('renders the links', () => {
expect(wrapper.queryByText('Hello, Foo!')).toBeInTheDocument()
expect(wrapper.queryByText('Hello, Bar!')).toBeInTheDocument()
})
})

describe('With custom link', () => {
beforeEach(() => {
onClickSpy = jest.fn()

wrapper = render(
<ContextMenu>
<ContextMenuItem
link={<CustomLink onClick={onClickSpy}>Click me!</CustomLink>}
/>
</ContextMenu>
)

wrapper.getByTestId('context-menu-custom-link').click()
})

it('invokes the onClick event when the custom link is clicked', () => {
expect(onClickSpy).toHaveBeenCalledTimes(1)
})
})

describe('With icons', () => {
beforeEach(() => {
wrapper = render(
<ContextMenu>
<ContextMenuItem
icon={<IconSettings data-testid="context-menu-item-icon" />}
link={<Link href="/hello-foo">Hello, Foo!</Link>}
/>
</ContextMenu>
)
})

it('renders the icons', () => {
expect(
wrapper.queryByTestId('context-menu-item-icon')
).toBeInTheDocument()
})
})

describe('With dividers', () => {
beforeEach(() => {
wrapper = render(
<ContextMenu>
<ContextMenuDivider />
<ContextMenuItem
icon={IconSettings}
link={<Link href="/hello-foo">Hello, Foo!</Link>}
/>
<ContextMenuDivider />
</ContextMenu>
)
})

it('renders the dividers', () => {
expect(wrapper.queryAllByTestId('context-menu-divider')).toHaveLength(2)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import styled from 'styled-components'
import { selectors } from '@royalnavy/design-tokens'

import { ComponentWithClass } from '../../common/ComponentWithClass'

const { color } = selectors

const StyledContextMenu = styled.ol`
width: 12rem;
padding: 0;
list-style-type: none;
background-color: ${color('neutral', 'white')};
border-radius: 4px;
border: 1px solid ${color('neutral', '200')};
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.07);
`

export const ContextMenu: React.FC<ComponentWithClass> = ({
className,
children,
}) => {
return <StyledContextMenu className={className}>{children}</StyledContextMenu>
}

ContextMenu.displayName = 'ContextMenu'
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import styled from 'styled-components'
import { selectors } from '@royalnavy/design-tokens'

const { color, spacing } = selectors

const StyledContextMenuDivider = styled.div`
width: 100%;
height: 1px;
background-color: ${color('neutral', '100')};
margin: ${spacing('2')} 0;
`

export const ContextMenuDivider: React.FC = () => {
return <StyledContextMenuDivider data-testid="context-menu-divider" />
}

ContextMenuDivider.displayName = 'ContextMenuDivider'
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'
import styled from 'styled-components'
import { selectors } from '@royalnavy/design-tokens'

import { NavItem } from '../../common/Nav'

interface ContextMenuItemProps extends NavItem {
icon?: React.ReactNode
}

const { color, fontSize, spacing } = selectors

const StyledContextMenuItem = styled.li`
overflow: hidden;
&:first-of-type {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&:last-of-type {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
> * {
display: flex;
padding: ${spacing('4')} ${spacing('6')};
overflow: hidden;
text-overflow: ellipsis;
text-wrap: none;
}
&:hover {
background-color: ${color('neutral', '000')};
> * {
text-decoration: none;
}
}
`

const StyledIcon = styled.div`
display: inline-flex;
align-items: center;
margin-right: ${spacing('4')};
svg {
color: ${color('neutral', '500')};
}
`

interface StyledTextProps {
hasIcon?: boolean
}

const StyledText = styled.div<StyledTextProps>`
color: ${color('neutral', '300')};
font-weight: 600;
font-size: ${fontSize('s')};
${({ hasIcon }) => !hasIcon && `margin-left: 1.5rem;`}
${StyledContextMenuItem}:hover & {
color: ${color('neutral', '400')};
}
`

export const ContextMenuItem: React.FC<ContextMenuItemProps> = ({
icon,
link,
}) => {
const linkElement = link as React.ReactElement

const item = React.cloneElement(linkElement, {
...link.props,
children: (
<>
{icon && <StyledIcon>{icon}</StyledIcon>}
<StyledText hasIcon={!!icon}>{linkElement.props.children}</StyledText>
</>
),
})

return <StyledContextMenuItem>{item}</StyledContextMenuItem>
}

ContextMenuItem.displayName = 'ContextMenuItem'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ContextMenu'
export * from './ContextMenuItem'
export * from './ContextMenuDivider'
1 change: 1 addition & 0 deletions packages/react-component-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './components/ButtonGroup'
export * from './components/CardFrame'
export * from './components/Checkbox'
export * from './components/CheckboxEnhanced'
export * from './components/ContextMenu'
export * from './components/DataList'
export * from './components/DatePicker'
export * from './components/Dialog'
Expand Down

0 comments on commit c8a32e0

Please sign in to comment.