From 1758f73ecd45f046c26694bf22eb2cdf49832c57 Mon Sep 17 00:00:00 2001 From: unix Date: Wed, 8 Apr 2020 07:52:10 +0800 Subject: [PATCH 1/2] feat(tooltip): append visible prop to control visible state manually --- components/tooltip/tooltip-content.tsx | 1 - components/tooltip/tooltip.tsx | 11 +++++++++-- pages/en-us/components/tooltip.mdx | 1 + pages/zh-cn/components/tooltip.mdx | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/components/tooltip/tooltip-content.tsx b/components/tooltip/tooltip-content.tsx index a674959..a9aadbd 100644 --- a/components/tooltip/tooltip-content.tsx +++ b/components/tooltip/tooltip-content.tsx @@ -73,7 +73,6 @@ const TooltipContent: React.FC> = React.memo(({ const preventHandler = (event: React.MouseEvent) => { event.stopPropagation() - event.preventDefault() event.nativeEvent.stopImmediatePropagation() } diff --git a/components/tooltip/tooltip.tsx b/components/tooltip/tooltip.tsx index 97c9e9f..a7baa09 100644 --- a/components/tooltip/tooltip.tsx +++ b/components/tooltip/tooltip.tsx @@ -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> = ({ children, initialVisible, text, offset, placement, portalClassName, enterDelay, leaveDelay, trigger, type, className, onVisibleChange, - hideArrow, ...props + hideArrow, visible: customVisible, ...props }) => { const timer = useRef() const ref = useRef(null) @@ -74,8 +75,14 @@ const Tooltip: React.FC> = ({ 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 (
Date: Wed, 8 Apr 2020 07:52:28 +0800 Subject: [PATCH 2/2] feat(popover): add component --- components/index.ts | 1 + components/popover/index.ts | 7 ++ components/popover/popover-item.tsx | 76 ++++++++++++++++ components/popover/popover.tsx | 61 +++++++++++++ components/utils/collections.ts | 9 ++ pages/en-us/components/popover.mdx | 134 +++++++++++++++++++++++++++ pages/zh-cn/components/popover.mdx | 135 ++++++++++++++++++++++++++++ 7 files changed, 423 insertions(+) create mode 100644 components/popover/index.ts create mode 100644 components/popover/popover-item.tsx create mode 100644 components/popover/popover.tsx create mode 100644 pages/en-us/components/popover.mdx create mode 100644 pages/zh-cn/components/popover.mdx diff --git a/components/index.ts b/components/index.ts index 9533146..e07eaf1 100644 --- a/components/index.ts +++ b/components/index.ts @@ -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' diff --git a/components/popover/index.ts b/components/popover/index.ts new file mode 100644 index 0000000..c4f4342 --- /dev/null +++ b/components/popover/index.ts @@ -0,0 +1,7 @@ +import Popover from './popover' +import PopoverItem from './popover-item' + +Popover.Item = PopoverItem +Popover.Option = PopoverItem + +export default Popover diff --git a/components/popover/popover-item.tsx b/components/popover/popover-item.tsx new file mode 100644 index 0000000..30313b9 --- /dev/null +++ b/components/popover/popover-item.tsx @@ -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, keyof Props> +export type PopoverItemProps = Props & typeof defaultProps & NativeAttrs + +const PopoverItem: React.FC> = React.memo(({ + children, line, title, className, ...props +}) => { + const theme = useTheme() + return ( + <> +
+ {children} + +
+ {title && } + + ) +}) + +export default withDefaults(PopoverItem, defaultProps) diff --git a/components/popover/popover.tsx b/components/popover/popover.tsx new file mode 100644 index 0000000..8e472c9 --- /dev/null +++ b/components/popover/popover.tsx @@ -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 + +const Popover: React.FC> = ({ + content, children, trigger, placement, portalClassName, ...props +}) => { + const theme = useTheme() + const textNode = useMemo(() => getReactNode(content), [content]) + + return ( + + {children} + + + ) +} + + +type PopoverComponent

= React.FC

& { + Item: typeof PopoverItem + Option: typeof PopoverItem +} + +type ComponentProps = Partial + & Omit + & Omit + +(Popover as PopoverComponent).defaultProps = defaultProps + +export default Popover as PopoverComponent diff --git a/components/utils/collections.ts b/components/utils/collections.ts index 04b6aae..db107ee 100644 --- a/components/utils/collections.ts +++ b/components/utils/collections.ts @@ -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)() +} diff --git a/pages/en-us/components/popover.mdx b/pages/en-us/components/popover.mdx new file mode 100644 index 0000000..e4aa437 --- /dev/null +++ b/pages/en-us/components/popover.mdx @@ -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. + + { + const content = () => ( +

+ A hyperlink + + External link +
+ ) + return ( + + Menu + + ) +} +`} /> + + { + const content = () => ( + <> + + User Settings + + + A hyperlink + + + A hyperlink for edit profile + + + + Command-Line + + + ) + return ( + + Menu + + ) +} +`} /> + + { + const [visible, setVisible] = useState(false) + const changeHandler = (next) => { + setVisible(next) + } + const content = () => ( +
+ setVisible(false)}>Click to close + + Nothing +
+ ) + return ( + + Menu + + ) +} +`} /> + + +Popover.Props + +| 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', ...` | - | + +Popover.Item + +| Attribute | Description | Type | Accepted values | Default +| ---------- | ---------- | ---- | -------------- | ------ | +| **line** | show a line | `boolean` | - | `false` | +| **title** | show text with title style | `boolean` | - | `false` | + +Placement + +```ts +type Placement = 'top' + | 'topStart', + | 'topEnd', + | 'left', + | 'leftStart', + | 'leftEnd', + | 'bottom', + | 'bottomStart', + | 'bottomEnd', + | 'right', + | 'rightStart', + | 'rightEnd', +``` + + + +export default ({ children }) => {children} diff --git a/pages/zh-cn/components/popover.mdx b/pages/zh-cn/components/popover.mdx new file mode 100644 index 0000000..6a733e3 --- /dev/null +++ b/pages/zh-cn/components/popover.mdx @@ -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 / 气泡卡片 + +通过点击或鼠标移入触发的气泡风格弹出层。 + + { + const content = () => ( +
+ 一个超链接 + + 外部链接 +
+ ) + return ( + + 菜单 + + ) +} +`} /> + + { + const content = () => ( + <> + + 用户设置 + + + 一个超链接 + + + 前往修改用户配置 + + + + 命令行工具 + + + ) + return ( + + 菜单 + + ) +} +`} /> + + { + const [visible, setVisible] = useState(false) + const changeHandler = (next) => { + setVisible(next) + } + const content = () => ( +
+ setVisible(false)}>点击关闭 + + 不关闭 +
+ ) + return ( + + 菜单 + + ) +} +`} /> + + +Popover.Props + +| 属性 | 描述 | 类型 | 推荐值 | 默认 +| ---------- | ---------- | ---- | -------------- | ------ | +| **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', ...` | - | + +Popover.Item + +| 属性 | 描述 | 类型 | 推荐值 | 默认 +| ---------- | ---------- | ---- | -------------- | ------ | +| **line** | 显示线条 | `boolean` | - | `false` | +| **title** | 用标题的样式展示文字 | `boolean` | - | `false` | + +Placement + +```ts +type Placement = 'top' + | 'topStart', + | 'topEnd', + | 'left', + | 'leftStart', + | 'leftEnd', + | 'bottom', + | 'bottomStart', + | 'bottomEnd', + | 'right', + | 'rightStart', + | 'rightEnd', +``` + + + +export default ({ children }) => {children}