-
Notifications
You must be signed in to change notification settings - Fork 333
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #253 from unix/breadcrumbs
feat: add Breadcrumbs component
- Loading branch information
Showing
12 changed files
with
656 additions
and
3 deletions.
There are no files selected for viewing
85 changes: 85 additions & 0 deletions
85
components/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>" | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.