Skip to content

Commit

Permalink
Merge pull request #253 from unix/breadcrumbs
Browse files Browse the repository at this point in the history
feat: add Breadcrumbs component
  • Loading branch information
unix authored Jun 1, 2020
2 parents 3c18b7f + 4715b78 commit 7665758
Show file tree
Hide file tree
Showing 12 changed files with 656 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Breadcrumbs should redefined all separators 1`] = `
"<nav class=\\"\\"><span class=\\"breadcrums-item \\">test-1</span><div class=\\"separator \\">*<style>
.separator {
display: inline-flex;
margin: 0 8px;
user-select: none;
pointer-events: none;
align-items: center;
}
</style></div><span class=\\"breadcrums-item \\">test-2</span><style>
nav {
margin: 0;
padding: 0;
line-height: inherit;
color: #888;
font-size: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
}
nav :global(.link:hover) {
color: rgba(0, 112, 243, 0.85);
}
nav > :global(span:last-of-type) {
color: #444;
}
nav > :global(.separator:last-child) {
display: none;
}
nav :global(svg) {
width: 1em;
height: 1em;
margin: 0 4px;
}
nav :global(.breadcrums-item) {
display: inline-flex;
align-items: center;
}
</style></nav>"
`;

exports[`Breadcrumbs should render correctly 1`] = `
"<nav class=\\"\\"><span class=\\"breadcrums-item \\">test-1</span><style>
nav {
margin: 0;
padding: 0;
line-height: inherit;
color: #888;
font-size: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
}
nav :global(.link:hover) {
color: rgba(0, 112, 243, 0.85);
}
nav > :global(span:last-of-type) {
color: #444;
}
nav > :global(.separator:last-child) {
display: none;
}
nav :global(svg) {
width: 1em;
height: 1em;
margin: 0 4px;
}
nav :global(.breadcrums-item) {
display: inline-flex;
align-items: center;
}
</style></nav>"
`;
75 changes: 75 additions & 0 deletions components/breadcrumbs/__tests__/breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react'
import { mount } from 'enzyme'
import { Breadcrumbs } from 'components'

describe('Breadcrumbs', () => {
it('should render correctly', () => {
const wrapper = mount(
<Breadcrumbs>
<Breadcrumbs.Item>test-1</Breadcrumbs.Item>
</Breadcrumbs>,
)
expect(wrapper.html()).toMatchSnapshot()
expect(() => wrapper.unmount()).not.toThrow()
})

it('should redefined all separators', () => {
const wrapper = mount(
<Breadcrumbs separator="*">
<Breadcrumbs.Item>test-1</Breadcrumbs.Item>
<Breadcrumbs.Item>test-2</Breadcrumbs.Item>
</Breadcrumbs>,
)
expect(wrapper.html()).toMatchSnapshot()
expect(wrapper.html()).toContain('*')
expect(() => wrapper.unmount()).not.toThrow()
})

it('the specified separator should be redefined', () => {
const wrapper = mount(
<Breadcrumbs separator="*">
<Breadcrumbs.Item>test-1</Breadcrumbs.Item>
<Breadcrumbs.Separator>%</Breadcrumbs.Separator>
<Breadcrumbs.Item>test-2</Breadcrumbs.Item>
</Breadcrumbs>,
)
expect(wrapper.html()).toContain('%')
})

it('should render string when href missing', () => {
let wrapper = mount(
<Breadcrumbs>
<Breadcrumbs.Item>test-1</Breadcrumbs.Item>
</Breadcrumbs>,
)
let dom = wrapper.find('.breadcrums-item').at(0).getDOMNode()
expect(dom.tagName).toEqual('SPAN')

wrapper = mount(
<Breadcrumbs>
<Breadcrumbs.Item href="">test-1</Breadcrumbs.Item>
</Breadcrumbs>,
)
dom = wrapper.find('.breadcrums-item').at(0).getDOMNode()
expect(dom.tagName).toEqual('A')

wrapper = mount(
<Breadcrumbs>
<Breadcrumbs.Item nextLink>test-1</Breadcrumbs.Item>
</Breadcrumbs>,
)
dom = wrapper.find('.breadcrums-item').at(0).getDOMNode()
expect(dom.tagName).toEqual('A')
})

it('should trigger click event', () => {
const handler = jest.fn()
const wrapper = mount(
<Breadcrumbs>
<Breadcrumbs.Item onClick={handler}>test-1</Breadcrumbs.Item>
</Breadcrumbs>,
)
wrapper.find('.breadcrums-item').at(0).simulate('click')
expect(handler).toHaveBeenCalled()
})
})
61 changes: 61 additions & 0 deletions components/breadcrumbs/breadcrumbs-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Link from '../link'
import { Props as LinkBasicProps } from '../link/link'
import React, { useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import { pickChild } from '../utils/collections'
import BreadcrumbsSeparator from './breadcrumbs-separator'

interface Props {
href?: string
nextLink?: boolean
onClick?: (event: React.MouseEvent) => void
className?: string
}

const defaultProps = {
nextLink: false,
className: '',
}

type NativeAttrs = Omit<React.AnchorHTMLAttributes<any>, keyof Props>
type NativeLinkAttrs = Omit<NativeAttrs, keyof LinkBasicProps>
export type BreadcrumbsProps = Props & typeof defaultProps & NativeLinkAttrs

const BreadcrumbsItem = React.forwardRef<
HTMLAnchorElement,
React.PropsWithChildren<BreadcrumbsProps>
>(
(
{ href, nextLink, onClick, children, className, ...props },
ref: React.Ref<HTMLAnchorElement>,
) => {
const isLink = useMemo(() => href !== undefined || nextLink, [href, nextLink])
const [withoutSepChildren] = pickChild(children, BreadcrumbsSeparator)
const clickHandler = (event: React.MouseEvent) => {
onClick && onClick(event)
}

if (!isLink) {
return (
<span className={`breadcrums-item ${className}`} onClick={clickHandler}>
{withoutSepChildren}
</span>
)
}

return (
<Link
className={`breadcrums-item ${className}`}
href={href}
onClick={clickHandler}
ref={ref}
{...props}>
{withoutSepChildren}
</Link>
)
},
)

const MemoBreadcrumbsItem = React.memo(BreadcrumbsItem)

export default withDefaults(MemoBreadcrumbsItem, defaultProps)
37 changes: 37 additions & 0 deletions components/breadcrumbs/breadcrumbs-separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'

interface Props {
className?: string
}

const defaultProps = {
className: '',
}

type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type BreadcrumbsProps = Props & typeof defaultProps & NativeAttrs

const BreadcrumbsSeparator: React.FC<React.PropsWithChildren<BreadcrumbsProps>> = ({
children,
className,
}) => {
return (
<div className={`separator ${className}`}>
{children}
<style jsx>{`
.separator {
display: inline-flex;
margin: 0 8px;
user-select: none;
pointer-events: none;
align-items: center;
}
`}</style>
</div>
)
}

const MemoBreadcrumbsSeparator = React.memo(BreadcrumbsSeparator)

export default withDefaults(MemoBreadcrumbsSeparator, defaultProps)
114 changes: 114 additions & 0 deletions components/breadcrumbs/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { ReactNode, useMemo } from 'react'
import useTheme from '../styles/use-theme'
import BreadcrumbsItem from './breadcrumbs-item'
import BreadcrumbsSeparator from './breadcrumbs-separator'
import { addColorAlpha } from '../utils/color'
import { NormalSizes } from '../utils/prop-types'

interface Props {
size: NormalSizes
separator?: string | ReactNode
className?: string
}

const defaultProps = {
size: 'medium' as NormalSizes,
separator: '/',
className: '',
}

type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type BreadcrumbsProps = Props & typeof defaultProps & NativeAttrs

const getSize = (size: NormalSizes) => {
const sizes: { [key in NormalSizes]: string } = {
mini: '.75rem',
small: '.875rem',
medium: '1rem',
large: '1.125rem',
}
return sizes[size]
}

const Breadcrumbs: React.FC<React.PropsWithChildren<BreadcrumbsProps>> = ({
size,
separator,
children,
className,
}) => {
const theme = useTheme()
const fontSize = useMemo(() => getSize(size), [size])
const hoverColor = useMemo(() => {
return addColorAlpha(theme.palette.link, 0.85)
}, [theme.palette.link])

const childrenArray = React.Children.toArray(children)
const withSeparatorChildren = childrenArray.map((item, index) => {
if (!React.isValidElement(item)) return item
const last = childrenArray[index - 1]
const lastIsSeparator = React.isValidElement(last) && last.type === BreadcrumbsSeparator
const currentIsSeparator = item.type === BreadcrumbsSeparator
if (!lastIsSeparator && !currentIsSeparator && index > 0) {
return (
<React.Fragment key={index}>
<BreadcrumbsSeparator>{separator}</BreadcrumbsSeparator>
{item}
</React.Fragment>
)
}
return item
})

return (
<nav className={className}>
{withSeparatorChildren}
<style jsx>{`
nav {
margin: 0;
padding: 0;
line-height: inherit;
color: ${theme.palette.accents_4};
font-size: ${fontSize};
box-sizing: border-box;
display: flex;
align-items: center;
}
nav :global(.link:hover) {
color: ${hoverColor};
}
nav > :global(span:last-of-type) {
color: ${theme.palette.accents_6};
}
nav > :global(.separator:last-child) {
display: none;
}
nav :global(svg) {
width: 1em;
height: 1em;
margin: 0 4px;
}
nav :global(.breadcrums-item) {
display: inline-flex;
align-items: center;
}
`}</style>
</nav>
)
}

type MemoBreadcrumbsComponent<P = {}> = React.NamedExoticComponent<P> & {
Item: typeof BreadcrumbsItem
Separator: typeof BreadcrumbsSeparator
}
type ComponentProps = Partial<typeof defaultProps> &
Omit<Props, keyof typeof defaultProps> &
NativeAttrs

Breadcrumbs.defaultProps = defaultProps

export default React.memo(Breadcrumbs) as MemoBreadcrumbsComponent<ComponentProps>
8 changes: 8 additions & 0 deletions components/breadcrumbs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Breadcrumbs from './breadcrumbs'
import BreadcrumbsItem from './breadcrumbs-item'
import BreadcrumbsSeparator from './breadcrumbs-separator'

Breadcrumbs.Item = BreadcrumbsItem
Breadcrumbs.Separator = BreadcrumbsSeparator

export default Breadcrumbs
1 change: 1 addition & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ export { default as User } from './user'
export { default as Page } from './page'
export { default as Grid } from './grid'
export { default as ButtonGroup } from './button-group'
export { default as Breadcrumbs } from './breadcrumbs'
2 changes: 1 addition & 1 deletion components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useTheme from '../styles/use-theme'
import useWarning from '../utils/use-warning'
import LinkIcon from './icon'

interface Props {
export interface Props {
href?: string
color?: boolean
pure?: boolean
Expand Down
Loading

0 comments on commit 7665758

Please sign in to comment.