Merge pull request #71 from unix/popover

feat(popover): add component
This commit is contained in:
witt
2020-04-08 07:56:31 +08:00
committed by GitHub
11 changed files with 434 additions and 3 deletions

View File

@@ -46,3 +46,4 @@ export { default as Table } from './table'
export { default as Toggle } from './toggle'
export { default as Snippet } from './snippet'
export { default as Tooltip } from './tooltip'
export { default as Popover } from './popover'

View File

@@ -0,0 +1,7 @@
import Popover from './popover'
import PopoverItem from './popover-item'
Popover.Item = PopoverItem
Popover.Option = PopoverItem
export default Popover

View File

@@ -0,0 +1,76 @@
import React from 'react'
import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
interface Props {
line?: boolean
title?: boolean
}
const defaultProps = {
line: false,
title: false,
}
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type PopoverItemProps = Props & typeof defaultProps & NativeAttrs
const PopoverItem: React.FC<React.PropsWithChildren<PopoverItemProps>> = React.memo(({
children, line, title, className, ...props
}) => {
const theme = useTheme()
return (
<>
<div className={`item ${line ? 'line' : ''} ${title ? 'title' : ''} ${className}`} {...props}>
{children}
<style jsx>{`
.item {
display: flex;
justify-content: flex-start;
align-items: center;
padding: .5rem ${theme.layout.gap};
color: ${theme.palette.accents_5};
font-size: .875rem;
line-height: 1.25rem;
text-align: left;
transition: color 0.1s ease 0s, background-color 0.1s ease 0s;
width: max-content;
}
.item:hover {
color: ${theme.palette.foreground};
}
.item > :global(*) {
margin: 0;
}
.item.line {
line-height: 0;
height: 0;
padding: 0;
border-top: 1px solid ${theme.palette.border};
margin: .5rem 0;
width: 100%;
}
.item.title {
padding: 1.15rem;
font-weight: 500;
font-size: .83rem;
color: ${theme.palette.foreground};
}
.item.title:first-of-type {
padding-top: .6rem;
padding-bottom: .6rem;
}
`}</style>
</div>
{title && <PopoverItem line title={false} />}
</>
)
})
export default withDefaults(PopoverItem, defaultProps)

View File

@@ -0,0 +1,61 @@
import React, { useMemo } from 'react'
import useTheme from '../styles/use-theme'
import Tooltip, { TooltipProps } from '../tooltip/tooltip'
import PopoverItem from '../popover/popover-item'
import { Placement, TriggerTypes } from '../utils/prop-types'
import { getReactNode } from '../utils/collections'
interface Props {
content?: React.ReactNode | (() => React.ReactNode)
trigger?: TriggerTypes
placement?: Placement
}
const defaultProps = {
trigger: 'click' as TriggerTypes,
placement: 'bottom',
}
type ExcludeTooltipProps = {
type: any
text: any
trigger: any
placement: any,
}
export type PopoverProps = Props & Omit<TooltipProps, keyof ExcludeTooltipProps>
const Popover: React.FC<React.PropsWithChildren<PopoverProps>> = ({
content, children, trigger, placement, portalClassName, ...props
}) => {
const theme = useTheme()
const textNode = useMemo(() => getReactNode(content), [content])
return (
<Tooltip text={textNode} trigger={trigger} placement={placement}
portalClassName={`popover ${portalClassName}`}
{...props}>
{children}
<style jsx>{`
:global(.tooltip-content.popover > .inner) {
padding: ${theme.layout.gapHalf} 0;
text-align: center;
}
`}</style>
</Tooltip>
)
}
type PopoverComponent<P = {}> = React.FC<P> & {
Item: typeof PopoverItem
Option: typeof PopoverItem
}
type ComponentProps = Partial<typeof defaultProps>
& Omit<Props, keyof typeof defaultProps>
& Omit<TooltipProps, keyof ExcludeTooltipProps>
(Popover as PopoverComponent<ComponentProps>).defaultProps = defaultProps
export default Popover as PopoverComponent<ComponentProps>

View File

@@ -73,7 +73,6 @@ const TooltipContent: React.FC<React.PropsWithChildren<Props>> = React.memo(({
const preventHandler = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation()
event.preventDefault()
event.nativeEvent.stopImmediatePropagation()
}

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import withDefaults from '../utils/with-defaults'
import TooltipContent from './tooltip-content'
import useClickAway from '../utils/use-click-away'
@@ -8,6 +8,7 @@ interface Props {
text: string | React.ReactNode
type?: SnippetTypes
placement?: Placement
visible?: boolean
initialVisible?: boolean
hideArrow?: boolean
trigger?: TriggerTypes
@@ -39,7 +40,7 @@ export type TooltipProps = Props & typeof defaultProps & NativeAttrs
const Tooltip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
children, initialVisible, text, offset, placement, portalClassName,
enterDelay, leaveDelay, trigger, type, className, onVisibleChange,
hideArrow, ...props
hideArrow, visible: customVisible, ...props
}) => {
const timer = useRef<number>()
const ref = useRef<HTMLDivElement>(null)
@@ -74,8 +75,14 @@ const Tooltip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
const mouseEventHandler = (next: boolean) => trigger === 'hover' && changeVisible(next)
const clickEventHandler = () => trigger === 'click' && changeVisible(!visible)
useClickAway(ref, () => trigger === 'click' && changeVisible(false))
useEffect(() => {
if (customVisible === undefined) return
changeVisible(customVisible)
}, [customVisible])
return (
<div ref={ref} className={`tooltip ${className}`}
onClick={clickEventHandler}

View File

@@ -101,3 +101,12 @@ export const setChildrenIndex = (
return item
})
}
export const getReactNode = (
node?: React.ReactNode | (() => React.ReactNode),
): React.ReactNode => {
if (!node) return null
if (typeof node !== 'function') return node
return (node as () => React.ReactNode)()
}

View File

@@ -0,0 +1,134 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Popover, Spacer, Link } from 'components'
import { useState } from 'react'
export const meta = {
title: 'Popover',
group: 'Data Display',
}
## Popover
The floating box popped by clicking or hovering.
<Playground
scope={{ Popover, Spacer, Link }}
code={`
() => {
const content = () => (
<div style={{ padding: '0 10px' }}>
<Link pure href="#">A hyperlink</Link>
<Spacer y={.5} />
<Link pure color href="#">External link</Link>
</div>
)
return (
<Popover content={content}>
Menu
</Popover>
)
}
`} />
<Playground
title="Preset Item"
desc="Use preset `Item` component build layout."
scope={{ Popover, Spacer, Link }}
code={`
() => {
const content = () => (
<>
<Popover.Item title>
<span>User Settings</span>
</Popover.Item>
<Popover.Item>
<Link pure href="#">A hyperlink</Link>
</Popover.Item>
<Popover.Item>
<Link pure color href="#">A hyperlink for edit profile</Link>
</Popover.Item>
<Popover.Item line />
<Popover.Item>
<span>Command-Line</span>
</Popover.Item>
</>
)
return (
<Popover content={content}>
Menu
</Popover>
)
}
`} />
<Playground
title="Close Manual"
desc="You can control when to close the pop-up box."
scope={{ Popover, Spacer, Link, useState }}
code={`
() => {
const [visible, setVisible] = useState(false)
const changeHandler = (next) => {
setVisible(next)
}
const content = () => (
<div style={{ padding: '0 10px' }}>
<span onClick={() => setVisible(false)}>Click to close</span>
<Spacer y={.5} />
<span>Nothing</span>
</div>
)
return (
<Popover content={content} visible={visible}
onVisibleChange={changeHandler}>
Menu
</Popover>
)
}
`} />
<Attributes edit="/pages/en-us/components/popover.mdx">
<Attributes.Title>Popover.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **content** | content of pop-up | `ReactNode` `() => ReactNode` | - | - |
| **visible** | visible or not | `boolean` | - | `false` |
| **initialVisible** | visible on initial | `boolean` | - | `false` |
| **hideArrow** | hide arrow icon | `boolean` | - | `false` |
| **placement** | position of the popover relative to the target | [Placement](#placement) | - | `bottom` |
| **trigger** | tooltip trigger mode | `'click' / 'hover'` | - | `click` |
| **enterDelay**(ms) | delay before popover is shown | `number` | - | `100` |
| **leaveDelay**(ms) | delay before popover is hidden | `number` | - | `0` |
| **offset**(px) | distance between pop-up and target | `number` | - | `12` |
| **portalClassName** | className of pop-up box | `string` | - | - |
| **onVisibleChange** | call when visibility of the popover is changed | `(visible: boolean) => void` | - | - |
| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
<Attributes.Title alias="Popover.Option">Popover.Item</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **line** | show a line | `boolean` | - | `false` |
| **title** | show text with title style | `boolean` | - | `false` |
<Attributes.Title>Placement</Attributes.Title>
```ts
type Placement = 'top'
| 'topStart',
| 'topEnd',
| 'left',
| 'leftStart',
| 'leftEnd',
| 'bottom',
| 'bottomStart',
| 'bottomEnd',
| 'right',
| 'rightStart',
| 'rightEnd',
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -178,6 +178,7 @@ Displays additional information on hover.
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **text** | text of pop-up | `string` `React.ReactNode` | - | - |
| **visible** | visible or not | `boolean` | - | `false` |
| **initialVisible** | visible on initial | `boolean` | - | `false` |
| **hideArrow** | hide arrow icon | `boolean` | - | `false` |
| **type** | preset style type | [TooltipTypes](#tooltiptypes) | - | `default` |

View File

@@ -0,0 +1,135 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Popover, Spacer, Link } from 'components'
import { useState } from 'react'
export const meta = {
title: '气泡卡片 Popover',
group: '数据展示',
}
## Popover / 气泡卡片
通过点击或鼠标移入触发的气泡风格弹出层。
<Playground
desc="基础示例。"
scope={{ Popover, Spacer, Link }}
code={`
() => {
const content = () => (
<div style={{ padding: '0 10px' }}>
<Link pure href="#">一个超链接</Link>
<Spacer y={.5} />
<Link pure color href="#">外部链接</Link>
</div>
)
return (
<Popover content={content}>
菜单
</Popover>
)
}
`} />
<Playground
title="预置子选项"
desc="使用预置的 `Item` 组件完成弹出内容的布局。"
scope={{ Popover, Spacer, Link }}
code={`
() => {
const content = () => (
<>
<Popover.Item title>
<span>用户设置</span>
</Popover.Item>
<Popover.Item>
<Link pure href="#">一个超链接</Link>
</Popover.Item>
<Popover.Item>
<Link pure color href="#">前往修改用户配置</Link>
</Popover.Item>
<Popover.Item line />
<Popover.Item>
<span>命令行工具</span>
</Popover.Item>
</>
)
return (
<Popover content={content}>
菜单
</Popover>
)
}
`} />
<Playground
title="手动关闭"
desc="你可以控制何时手动地关闭弹出卡片。"
scope={{ Popover, Spacer, Link, useState }}
code={`
() => {
const [visible, setVisible] = useState(false)
const changeHandler = (next) => {
setVisible(next)
}
const content = () => (
<div style={{ padding: '0 10px' }}>
<span onClick={() => setVisible(false)}>点击关闭</span>
<Spacer y={.5} />
<span>不关闭</span>
</div>
)
return (
<Popover content={content} visible={visible}
onVisibleChange={changeHandler}>
菜单
</Popover>
)
}
`} />
<Attributes edit="/pages/zh-cn/components/popover.mdx">
<Attributes.Title>Popover.Props</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认
| ---------- | ---------- | ---- | -------------- | ------ |
| **content** | 气泡卡片内容 | `ReactNode` `() => ReactNode` | - | - |
| **visible** | 手动控制气泡的显示与隐藏 | `boolean` | - | `false` |
| **initialVisible** | 初始是否可见 | `boolean` | - | `false` |
| **hideArrow** | 隐藏箭头 | `boolean` | - | `false` |
| **placement** | 气泡卡片与目标的对齐方式 | [Placement](#placement) | - | `bottom` |
| **trigger** | 触发气泡卡片的方式 | `'click' / 'hover'` | - | `click` |
| **enterDelay**(ms) | 在提示显示前的延迟 | `number` | - | `100` |
| **leaveDelay**(ms) | 关闭提示前的延迟 | `number` | - | `0` |
| **offset**(px) | 提示框与目标之间的偏移 | `number` | - | `12` |
| **portalClassName** | 气泡卡片的类名 | `string` | - | - |
| **onVisibleChange** | 当气泡卡片状态改变时触发 | `(visible: boolean) => void` | - | - |
| ... | 原生属性 | `HTMLAttributes` | `'id', 'name', 'className', ...` | - |
<Attributes.Title alias="Popover.Option">Popover.Item</Attributes.Title>
| 属性 | 描述 | 类型 | 推荐值 | 默认
| ---------- | ---------- | ---- | -------------- | ------ |
| **line** | 显示线条 | `boolean` | - | `false` |
| **title** | 用标题的样式展示文字 | `boolean` | - | `false` |
<Attributes.Title>Placement</Attributes.Title>
```ts
type Placement = 'top'
| 'topStart',
| 'topEnd',
| 'left',
| 'leftStart',
| 'leftEnd',
| 'bottom',
| 'bottomStart',
| 'bottomEnd',
| 'right',
| 'rightStart',
| 'rightEnd',
```
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -178,6 +178,7 @@ export const meta = {
| 属性 | 描述 | 类型 | 推荐值 | 默认
| ---------- | ---------- | ---- | -------------- | ------ |
| **text** | 弹出框文字 | `string` `React.ReactNode` | - | - |
| **visible** | 手动控制提示框的显示与隐藏 | `boolean` | - | `false` |
| **initialVisible** | 初始是否可见 | `boolean` | - | `false` |
| **hideArrow** | 隐藏箭头 | `boolean` | - | `false` |
| **type** | 不同的文字提示类型 | [TooltipTypes](#tooltiptypes) | - | `default` |