未验证 提交 c3094934 编写于 作者: L lzzwoniu 提交者: GitHub

feat: PopOver组件 (#63)

* feat: popOver组件开发

* feat: tag和badge修改h2标签

* feat: 组件引入修改

* feat: review

* feat: review
上级 094f7fef
......@@ -7,7 +7,8 @@ const BadgeDemo = () => {
return (
<>
<div className="demo">
<CellGroup title="默认用法">
<h2>默认用法</h2>
<CellGroup>
<Cell>
<Badge value={8}>
<Avatar icon="my" shape="square"></Avatar>
......@@ -24,7 +25,8 @@ const BadgeDemo = () => {
</Cell>
</CellGroup>
<CellGroup title="最大值">
<h2>最大值</h2>
<CellGroup>
<Cell>
<Badge value={200} max={9}>
<Avatar icon="my" shape="square"></Avatar>
......@@ -38,7 +40,8 @@ const BadgeDemo = () => {
</Cell>
</CellGroup>
<CellGroup title="自定义颜色">
<h2>自定义颜色</h2>
<CellGroup>
<Cell>
<Badge
value={8}
......@@ -67,7 +70,8 @@ const BadgeDemo = () => {
</Cell>
</CellGroup>
<CellGroup title="自定义徽标内容">
<h2>自定义徽标内容</h2>
<CellGroup>
<Cell>
<Badge icons="checklist">
<Avatar icon="my" shape="square"></Avatar>
......@@ -81,7 +85,8 @@ const BadgeDemo = () => {
</Cell>
</CellGroup>
<CellGroup title="自定义位置">
<h2>自定义位置</h2>
<CellGroup>
<Cell>
<Badge value={8} top="5" right="5">
<Avatar icon="my" shape="square"></Avatar>
......@@ -95,7 +100,8 @@ const BadgeDemo = () => {
</Cell>
</CellGroup>
<CellGroup title="独立展示">
<h2>独立展示</h2>
<CellGroup>
<Cell>
<Badge value={8}></Badge>
<Badge value={76}></Badge>
......
import React, { HTMLAttributes } from 'react'
interface TriggerProps {
className?: string | undefined
// onMouseEnter: () => void
// onMouseLeave: () => void
// onFocus: () => void
// onClick: (e: any) => void
// onKeyPress?: () => void
forwardedRef: React.RefObject<HTMLElement>
}
interface TriggerState {}
/**
* 过滤ref
*/
export function fillRef<T>(ref: React.Ref<T>, node: T) {
if (typeof ref === 'function') {
ref(node)
} else if (typeof ref === 'object' && ref && 'current' in ref) {
;(ref as any).current = node
}
}
/**
* 将ref合并到一个ref函数中以支持ref传递
*/
export function composeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
return (node: T) => {
refs.forEach((ref) => {
fillRef(ref, node)
})
}
}
const ALL_HANDLERS = [
// 'onClick',
// 'onMouseDown',
// 'onTouchStart',
// 'onMouseEnter',
// 'onMouseLeave',
// 'onFocus',
// 'onBlur',
// 'onContextMenu',
]
export default class Trigger extends React.Component<TriggerProps, TriggerState> {
constructor(props: any) {
super(props)
// ALL_HANDLERS.forEach((h) => {
// ( this as any)[`fire${h}`] = (e: any) => {
// this.fireEvents(h, e)
// }
// })
}
fireEvents(type: string, e: Event) {
const childCallback = (this.props.children as React.ReactElement).props[type]
if (childCallback) {
childCallback(e)
}
const callback = (this.props as any)[type]
if (callback) {
callback(e)
}
}
render() {
const { children, className = '' } = this.props
const child = React.Children.only(children) as React.ReactElement
const newChildProps: HTMLAttributes<HTMLElement> & { key: string } = {
key: 'trigger',
}
// newChildProps.onClick = (e: any) => {
// this.fireEvents('onClick', e)
// }
// newChildProps.onMouseEnter = (e: any) => {
// this.fireEvents('onMouseEnter', e)
// }
// newChildProps.onMouseLeave = (e: any) => {
// this.fireEvents('onMouseLeave', e)
// }
// newChildProps.onFocus = (e: any) => {
// this.fireEvents('onMouseLeave', e)
// }
// newChildProps.onKeyPress = (e: any) => {
// this.fireEvents('onMouseLeave', e)
// }
if (child && child.props && child.props.className) {
newChildProps.className = className
}
const cloneProps: any = {
...newChildProps,
}
cloneProps.ref = composeRef(this.props.forwardedRef, (child as any).ref)
const trigger = React.cloneElement(child, cloneProps)
return trigger
}
}
import React, { useState } from 'react'
import { Popover } from './popover'
import Button from '@/packages/button'
import Icon from '@/packages/icon'
const BadgeDemo = () => {
let selfContentStyle = {
width: '195px',
display: 'flex',
flexWrap: 'wrap',
} as any
let selfContentItem = {
marginTop: '10px',
marginBottom: '10px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
} as any
let selfContentDesc = {
marginTop: '5px',
width: '60px',
fontSize: '10px',
textAlign: 'center',
} as any
let hTwo = {
marginTop: '30px',
marginBottom: '10px',
fontSize: '14px',
color: '#909ca4',
padding: '0 10px',
fontWeight: 400,
} as any
const itemList = [
{
name: '选项一',
},
{
name: '选项二',
},
{
name: '选项三',
},
]
const iconItemList = [
{
name: '选项一',
icon: 'my2',
},
{
name: '选项二',
icon: 'cart2',
},
{
name: '选项三',
icon: 'location2',
},
]
const itemListDisabled = [
{
name: '选项一',
disabled: true,
},
{
name: '选项二',
disabled: true,
},
{
name: '选项三',
},
]
const selfContent = [
{
name: 'service',
desc: '选项一',
},
{
name: 'notice',
desc: '选项二',
},
{
name: 'location',
desc: '选项三',
},
{
name: 'category',
desc: '选项四',
},
{
name: 'scan2',
desc: '选项五',
},
{
name: 'message',
desc: '选项六',
},
]
const [lightTheme, setLightTheme] = useState(false)
const [darkTheme, setDarkTheme] = useState(false)
const [showIcon, setShowIcon] = useState(false)
const [disableAction, setDisableAction] = useState(false)
const [topLocation, setTopLocation] = useState(false)
const [rightLocation, setRightLocation] = useState(false)
const [leftLocation, setLeftLocation] = useState(false)
const [customized, setCustomized] = useState(false)
return (
<>
<div className="demo">
<div>
<h6 style={hTwo}>基础用法</h6>
<Popover
visible={lightTheme}
onClick={() => {
lightTheme ? setLightTheme(false) : setLightTheme(true)
}}
list={itemList}
>
<Button type="primary" shape="square">
明朗风格
</Button>
</Popover>
<Popover
visible={darkTheme}
theme="dark"
onClick={() => {
darkTheme ? setDarkTheme(false) : setDarkTheme(true)
}}
list={itemList}
>
<Button type="primary" shape="square">
暗黑风格
</Button>
</Popover>
</div>
<div>
<h6 style={hTwo}>选项配置</h6>
<Popover
visible={showIcon}
theme="dark"
onClick={() => {
showIcon ? setShowIcon(false) : setShowIcon(true)
}}
list={iconItemList}
>
<Button type="primary" shape="square">
展示图标
</Button>
</Popover>
<Popover
visible={disableAction}
onClick={() => {
disableAction ? setDisableAction(false) : setDisableAction(true)
}}
list={itemListDisabled}
>
<Button type="primary" shape="square">
禁用选项
</Button>
</Popover>
</div>
<div>
<h6 style={hTwo}>自定义内容</h6>
<Popover
visible={customized}
onClick={() => {
customized ? setCustomized(false) : setCustomized(true)
}}
>
<Button type="primary" shape="square">
自定义内容
</Button>
{customized ? (
<div className="self-content" style={selfContentStyle}>
{selfContent.map((item: any) => {
return (
<div className="self-content-item" style={selfContentItem} key={item.name}>
<Icon name={item.name} size="15"></Icon>
<div className="self-content-desc" style={selfContentDesc}>
{item.desc}
</div>
</div>
)
})}
</div>
) : (
''
)}
</Popover>
</div>
<div>
<h6 style={hTwo}>位置自定义</h6>
<Popover
visible={topLocation}
location="top"
theme="dark"
onClick={() => {
topLocation ? setTopLocation(false) : setTopLocation(true)
}}
list={iconItemList}
>
<Button type="primary" shape="square">
向上弹出
</Button>
</Popover>
<h2 style={hTwo}></h2>
<Popover
visible={rightLocation}
location="right"
theme="dark"
onClick={() => {
rightLocation ? setRightLocation(false) : setRightLocation(true)
}}
list={iconItemList}
>
<Button type="primary" shape="square">
向右弹出
</Button>
</Popover>
<Popover
visible={leftLocation}
location="left"
theme="dark"
onClick={() => {
leftLocation ? setLeftLocation(false) : setLeftLocation(true)
}}
list={iconItemList}
>
<Button type="primary" shape="square">
向左弹出
</Button>
</Popover>
</div>
</div>
</>
)
}
export default BadgeDemo
# Popover 气泡弹出框
### 介绍
点击或在元素上悬停鼠标,弹出气泡卡片浮层。
### 安装
``` javascript
import { Popover } from '@nutui/nutui-react';
```
### 代码实例
### 基本用法
Popover 支持明朗和暗黑两种风格,默认为明朗风格,将 theme 属性设置为 dark 可切换为暗黑风格。
```tsx
<Popover
visible={lightTheme}
onClick={()=>{lightTheme ? setLightTheme(false) : setLightTheme(true)}}
list={itemList}>
<Button type="primary" shape="square">明朗风格</Button>
</Popover>
<Popover
visible={darkTheme}
theme="dark"
onClick={()=>{darkTheme ? setDarkTheme(false) : setDarkTheme(true)}}
list={itemList}>
<Button type="primary" shape="square">暗黑风格</Button>
</Popover>
```
```javascript
const [lightTheme, setLightTheme] = useState(false)
const [darkTheme, setDarkTheme] = useState(false)
const itemList = [
{name: '选项一'},
{name: '选项二'},
{name: '选项三'}];
```
### 选项配置
```tsx
<Popover
visible={showIcon}
theme="dark"
onClick={()=>{showIcon ? setShowIcon(false) : setShowIcon(true)}}
list={iconItemList}>
<Button type="primary" shape="square">展示图标</Button>
</Popover>
<Popover
visible={disableAction}
onClick={()=>{disableAction ? setDisableAction(false) : setDisableAction(true)}}
list={itemListDisabled}>
<Button type="primary" shape="square">禁用选项</Button>
</Popover>
```
```javascript
const [showIcon, setShowIcon] = useState(false)
const [disableAction, setDisableAction] = useState(false)
const iconItemList= [
{name: '选项一',icon: 'my2'},
{name: '选项二',icon: 'cart2'},
{name: '选项三',icon: 'location2'}
];
const itemListDisabled=[
{name: '选项一',disabled: true},
{name: '选项二', disabled: true},
{name: '选项三'}
];
```
### 自定义内容
```tsx
<Popover
visible={customized}
onClick={()=>{customized ? setCustomized(false) : setCustomized(true)}}>
<Button type="primary" shape="square">自定义内容</Button>
{
customized ?
<div className="self-content" style={selfContentStyle}>
{
selfContent.map((item: any)=>{
return <div className="self-content-item" style={selfContentItem} key={item.name}>
<Icon name={item.name} size="15"></Icon>
<div className="self-content-desc" style={selfContentDesc}>{ item.desc }</div>
</div>
})
}
</div> : ''
}
</Popover>
```
```javascript
const [customized, setCustomized] = useState(false)
const selfContent= [
{
name: 'service',
desc: '选项一'
},
{
name: 'notice',
desc: '选项二'
},
{
name: 'location',
desc: '选项三'
},
{
name: 'category',
desc: '选项四'
},
{
name: 'scan2',
desc: '选项五'
},
{
name: 'message',
desc: '选项六'
}
];
```
### 位置自定义
```tsx
<Popover
visible={topLocation}
location="top"
theme="dark"
onClick={()=>{topLocation ? setTopLocation(false) : setTopLocation(true)}}
list={iconItemList}>
<Button type="primary" shape="square">向上弹出</Button>
</Popover>
<Popover
visible={rightLocation}
location="right"
theme="dark"
onClick={()=>{rightLocation ? setRightLocation(false) : setRightLocation(true)}}
list={iconItemList}>
<Button type="primary" shape="square">向右弹出</Button>
</Popover>
<Popover
visible={leftLocation}
location="left"
theme="dark"
onClick={()=>{leftLocation ? setLeftLocation(false) : setLeftLocation(true)}}
list={iconItemList}>
<Button type="primary" shape="square">向左弹出</Button>
</Popover>
```
```javascript
const [topLocation, setTopLocation] = useState(false)
const [rightLocation, setRightLocation] = useState(false)
const [leftLocation, setLeftLocation] = useState(false)
const iconItemList= [
{name: '选项一',icon: 'my2'},
{name: '选项二',icon: 'cart2'},
{name: '选项三',icon: 'location2'}
];
```
## API
### Props
| 字段 | 说明 | 类型 | 默认值 |
|----------------|---------------------------------|---------|------------|
| list | 选项列表 | List[] | [] |
| visible | 是否展示气泡弹出层 | boolean | false |
| theme | 主题风格,可选值为 dark | string | `light` |
| location | 弹出位置,可选值为 top,left,right | string | `bottom` |
### List 数据结构
List 属性是一个由对象构成的数组,数组中的每个对象配置一列,对象可以包含以下值:
| 键名 | 说明 | 类型 | 默认值 |
|----------------|----------------------|----------|--------|
| name | 选项文字 | string | - |
| icon | nut-icon 图标名称 | string | - |
| disabled | 是否为禁用状态 | boolean | false |
### Events
| 名称 | 说明 |
|---------|--------------|
| onClick | 打开(关闭)菜单时触发 |
import { Popover } from './popover'
export default Popover
.popBox {
// background: skyblue;
// // background: #fff;
// padding: 20px;
.titBox {
// margin-bottom: 20px;
}
}
.nut-popover--dark,
.nut-popover {
position: relative;
display: inline-block;
margin-right: 20px;
.more-background {
background: $popover-white-background-color;
opacity: 0;
position: fixed;
width: 100%;
height: 1000px;
z-index: 10;
left: 0;
}
.popoverContent--left,
.popoverContent--right,
.popoverContent--top,
.popoverContent {
z-index: 12;
background: $popover-white-background-color;
border-radius: 5px;
opacity: 1;
font-size: 14px;
font-family: PingFangSC;
font-weight: normal;
color: $popover-primary-text-color;
position: absolute;
.popoverArrow {
position: absolute;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid $popover-white-background-color;
}
.title-item {
display: flex;
align-items: center;
padding-bottom: 8px;
margin: 8px;
border-bottom: 1px solid $popover-border-bottom-color;
&:first-child {
margin-top: 15px;
}
&:last-child {
margin-bottom: 2px;
border-bottom: none;
}
.title-name {
margin-right: 12px;
margin-left: 8px;
width: 100%;
}
}
}
.popoverContent--top {
.popoverArrow--top {
position: absolute;
top: auto;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 10px solid $popover-white-background-color;
border-bottom: 10px solid transparent;
}
}
.popoverContent--left {
.popoverArrow--left {
position: absolute;
border-left: 10px solid $popover-white-background-color;
border-right: 10px solid transparent;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
}
}
.popoverContent--right {
.popoverArrow--right {
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid $popover-white-background-color;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
}
}
.disabled {
color: $popover-disable-color;
cursor: not-allowed;
}
}
.nut-popover--dark {
background: $popover-dark-background-color;
color: $popover-white-background-color;
.popoverContent--left,
.popoverContent--right,
.popoverContent--top,
.popoverContent {
background: $popover-dark-background-color;
color: $popover-white-background-color;
.popoverArrow {
border-bottom: 10px solid $popover-dark-background-color;
}
}
.popoverContent--top {
.popoverArrow--top {
position: absolute;
top: auto;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 10px solid $popover-dark-background-color;
border-bottom: 10px solid transparent;
}
}
.popoverContent--left {
.popoverArrow--left {
position: absolute;
border-left: 10px solid $popover-dark-background-color;
border-right: 10px solid transparent;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
}
}
.popoverContent--right {
.popoverArrow--right {
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid $popover-dark-background-color;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
}
}
}
import React, {
CSSProperties,
FunctionComponent,
MouseEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import Trigger from './Trigger'
import ReactDOM from 'react-dom'
import './popover.scss'
import Icon from '@/packages/icon'
export interface PopoverProps {
list: Array<any>
theme: string
location: string
visible: boolean
className: string
style?: CSSProperties
onClick: (e: MouseEvent) => void
}
export function findDOMNode<T = HTMLElement>(node: React.ReactInstance | HTMLElement): T {
if (node instanceof HTMLElement) {
return node as unknown as T
}
return ReactDOM.findDOMNode(node) as unknown as T
}
const getEleAttr = (ele: HTMLElement | Element) => {
if (ele && ele.getBoundingClientRect) {
return ele.getBoundingClientRect()
}
return null
}
export type PopoverType = 'default' | 'primary' | 'success' | 'warning' | 'danger'
const defaultProps = {
list: [],
theme: 'light',
location: 'bottom',
visible: false,
className: '',
onClick: (e: MouseEvent) => {},
} as PopoverProps
export const Popover: FunctionComponent<Partial<PopoverProps>> = (props) => {
const { children, list, theme, location, visible, className, style, onClick, ...reset } = {
...defaultProps,
...props,
}
const goodItem = useRef(null)
let aa = goodItem.current && findDOMNode(goodItem.current)
setTimeout(() => {
if (aa) {
setElWidth((getEleAttr(aa) as any).width)
setElHeight((getEleAttr(aa) as any).height)
}
})
const [classes, setClasses] = useState('')
const [elWidth, setElWidth] = useState(0)
const [elHeight, setElHeight] = useState(0)
const [popoverContent, setPopoverContent] = useState('')
const [popoverArrow, setPopoverArrow] = useState('')
useEffect(() => {
setClasses(classes_self())
setPopoverContent(popoverContent_self())
setPopoverArrow(popoverArrow_self())
}, [list, theme])
const getStyle = () => {
const style: CSSProperties = {}
if (location == 'top') {
style.bottom = elHeight + 20
style.left = 0
} else if (location == 'right') {
style.top = 0
style.right = -elWidth - 20
} else if (location == 'left') {
style.top = 0
style.left = -elWidth - 20
} else {
style.top = elHeight + 20
style.left = 0
}
style.top = style.top + 'px'
style.left = style.left + 'px'
style.bottom = style.bottom + 'px'
style.right = style.right + 'px'
return style
}
const getArrowStyle = () => {
const style: CSSProperties = {}
if (location == 'top') {
style.bottom = -20
style.left = elWidth / 2
} else if (location == 'right') {
style.top = 20
style.left = -20
} else if (location == 'left') {
style.top = 20
style.right = -20
} else {
style.left = elWidth / 2
style.top = -20
}
style.top = style.top + 'px'
style.left = style.left + 'px'
style.bottom = style.bottom + 'px'
style.right = style.right + 'px'
return style
}
const classes_self = () => {
const prefixCls = 'nut-popover'
return `${prefixCls}
${theme ? `${prefixCls}--${theme}` : ''}`
}
const popoverContent_self = () => {
const prefixCls = 'popoverContent'
return `${prefixCls}
${location ? `${prefixCls}--${location}` : ''}`
}
const popoverArrow_self = () => {
const prefixCls = 'popoverArrow'
return `${prefixCls}
${location ? `${prefixCls}--${location}` : ''}`
}
// const showPopup = props.visible
const handleClick = (e: any) => {
if (props.onClick) {
props.onClick(e)
}
}
return (
<div className={`${classes} ${className}`} style={{ ...style }} {...reset}>
<Trigger forwardedRef={goodItem}>
<div onClick={(e) => handleClick(e)}>
{Array.isArray(children) ? children[0] : children}
{visible ? (
<div className={`${popoverContent}`} style={getStyle()}>
<div className={`${popoverArrow}`} style={getArrowStyle()}>
{' '}
</div>
{Array.isArray(children) ? children[1] : ''}
{list.map((item) => {
return (
<div key={item.name} className={`title-item ${item.disabled ? 'disabled' : ''}`}>
{item.icon ? <Icon className="item-img" name={item.icon}></Icon> : ''}
<div className="title-name">{item.name}</div>
</div>
)
})}
</div>
) : null}
</div>
</Trigger>
</div>
)
}
Popover.defaultProps = defaultProps
Popover.displayName = 'NutPopover'
......@@ -10,14 +10,16 @@ const TagDemo = () => {
return (
<>
<div className="demo">
<CellGroup title="基础用法">
<h2>基础用法</h2>
<CellGroup>
<Cell title="primary类型" extra={<Tag type="primary">标签</Tag>}></Cell>
<Cell title="success类型" extra={<Tag type="success">标签</Tag>}></Cell>
<Cell title="danger类型" extra={<Tag type="danger">标签</Tag>}></Cell>
<Cell title="warning类型" extra={<Tag type="warning">标签</Tag>}></Cell>
</CellGroup>
<CellGroup title="样式风格">
<h2>样式风格</h2>
<CellGroup>
<Cell title="空心样式" extra={<Tag plain>标签</Tag>}></Cell>
<Cell
title="圆角样式"
......@@ -45,7 +47,8 @@ const TagDemo = () => {
></Cell>
</CellGroup>
<CellGroup title="自定义">
<h2>自定义</h2>
<CellGroup>
<Cell title="背景颜色" extra={<Tag color="#FA685D">标签</Tag>}></Cell>
<Cell
title="文字颜色"
......
......@@ -306,6 +306,13 @@ $badge-background-color: linear-gradient(
$badge-font-size: $font-size-1 !default;
$badge-default-background-color: rgba(255, 255, 255, 1) !default;
//popover
$popover-white-background-color: rgba(255, 255, 255, 1) !default;
$popover-dark-background-color: rgba(75, 76, 77, 1) !default;
$popover-border-bottom-color: rgba(229, 229, 229, 1) !default;
$popover-primary-text-color: rgba(51, 51, 51, 1) !default;
$popover-disable-color: rgba(154, 155, 157, 1) !default;
//pagination
$pagination-color: $primary-color !default;
$pagination-font-size: $font-size-2 !default;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册