-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(popover): implement new popover with positioner
- Loading branch information
Showing
6 changed files
with
298 additions
and
4 deletions.
There are no files selected for viewing
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,82 @@ | ||
--- | ||
name: Popover | ||
menu: Lab | ||
route: /lab/popover | ||
--- | ||
|
||
import { PropsTable, Playground, Link } from 'docz'; | ||
import { Button, Position } from 'tailor-ui'; | ||
import { Popover } from '@tailor-ui/lab'; | ||
|
||
# Popover | ||
|
||
The floating card popped by clicking or hovering. | ||
|
||
## When To Use | ||
|
||
A simple popup menu to provide extra information or operations. | ||
|
||
Comparing with `Tooltip`, besides information `Popover` card can also provide action elements like links and buttons. | ||
|
||
## Examples | ||
|
||
```js | ||
import { Popover } from 'tailor-ui'; | ||
``` | ||
|
||
### Basic | ||
|
||
<Playground> | ||
<Popover title="Popover Title" content="Popover Content"> | ||
<Button>Button</Button> | ||
</Popover> | ||
</Playground> | ||
|
||
### With position right | ||
|
||
<Playground> | ||
<Popover | ||
position={Position.RIGHT} | ||
title="Popover Title" | ||
content="Popover Content" | ||
> | ||
<Button>Button</Button> | ||
</Popover> | ||
</Playground> | ||
|
||
### With content hide | ||
|
||
<Playground> | ||
<Popover | ||
position={Position.RIGHT} | ||
title="Title" | ||
content={hide => ( | ||
<div> | ||
Popover Content | ||
<br /> | ||
<Button size="sm" onClick={hide}> | ||
Hide Popover | ||
</Button> | ||
</div> | ||
)} | ||
> | ||
<Button>Button</Button> | ||
</Popover> | ||
</Playground> | ||
|
||
### With onVisibleChange | ||
|
||
<Playground> | ||
<Popover | ||
position={Position.RIGHT} | ||
title="Title" | ||
content="Popover content" | ||
onVisibleChange={console.log} | ||
> | ||
<Button>Button</Button> | ||
</Popover> | ||
</Playground> | ||
|
||
## API | ||
|
||
<PropsTable of={Popover} /> |
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,181 @@ | ||
import React, { | ||
CSSProperties, | ||
FunctionComponent, | ||
ReactNode, | ||
RefObject, | ||
cloneElement, | ||
forwardRef, | ||
isValidElement, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
import { animated } from 'react-spring'; | ||
|
||
import { | ||
Heading, | ||
Position, | ||
Positioner, | ||
Positions, | ||
useClickOutside, | ||
useKeydown, | ||
} from 'tailor-ui'; | ||
|
||
import { PopoverContent, PopoverHeader, StyledPopover } from './styles'; | ||
|
||
interface PopoverPopup { | ||
style: CSSProperties; | ||
title: ReactNode | ((handleClose: () => void) => ReactNode); | ||
content: ReactNode | ((handleClose: () => void) => ReactNode); | ||
handleClose: () => void; | ||
} | ||
|
||
const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopup>( | ||
function PopoverPopup( | ||
{ style, title, content, handleClose, ...otherProps }, | ||
ref | ||
) { | ||
return ( | ||
<animated.div style={style} ref={ref}> | ||
<StyledPopover {...otherProps}> | ||
{title && ( | ||
<PopoverHeader> | ||
<Heading.h6> | ||
{title instanceof Function ? title(handleClose) : title} | ||
</Heading.h6> | ||
</PopoverHeader> | ||
)} | ||
<PopoverContent> | ||
{content instanceof Function ? content(handleClose) : content} | ||
</PopoverContent> | ||
</StyledPopover> | ||
</animated.div> | ||
); | ||
} | ||
); | ||
|
||
export interface PopoverProps { | ||
/** | ||
* Whether the floating popover card is visible by default. Only support when the trigger is `click` | ||
*/ | ||
defaultVisible?: boolean; | ||
/** | ||
* Whether the floating popover card is visible. Only support when the trigger is `click` | ||
*/ | ||
visible?: boolean; | ||
/** | ||
* Callback executed when visibility of the popover card is changed | ||
*/ | ||
onVisibleChange?: (visible: boolean) => void; | ||
/** | ||
* The position base on the children component | ||
*/ | ||
position?: Positions; | ||
title?: ReactNode | ((handleClose: () => void) => ReactNode); | ||
/** | ||
* A string or react component inside this popover. | ||
* If you are using click to trigger, it can be a | ||
* function that with `hide` callback as first argument | ||
*/ | ||
content: ReactNode | ((handleClose: () => void) => ReactNode); | ||
} | ||
|
||
const Popover: FunctionComponent<PopoverProps> = ({ | ||
children, | ||
position = Position.TOP, | ||
title, | ||
content, | ||
defaultVisible = false, | ||
visible: visibleFromProps, | ||
onVisibleChange, | ||
}) => { | ||
const childrenRef = useRef(null); | ||
const popupRef = useRef(null); | ||
const [visibleFromSelf, setVisibleFromSelf] = useState(defaultVisible); | ||
|
||
const hasVisibleFromProps = typeof visibleFromProps !== 'undefined'; | ||
|
||
const visible = hasVisibleFromProps ? visibleFromProps : visibleFromSelf; | ||
|
||
const handleOpen = () => { | ||
if (onVisibleChange) { | ||
onVisibleChange(true); | ||
} | ||
|
||
if (!hasVisibleFromProps) { | ||
setVisibleFromSelf(true); | ||
} | ||
}; | ||
|
||
const handleClose = () => { | ||
if (onVisibleChange) { | ||
onVisibleChange(false); | ||
} | ||
|
||
if (!hasVisibleFromProps) { | ||
setVisibleFromSelf(false); | ||
} | ||
}; | ||
|
||
const toggle = () => { | ||
if (visible) { | ||
handleClose(); | ||
} else { | ||
handleOpen(); | ||
} | ||
}; | ||
|
||
useClickOutside({ | ||
listening: visible, | ||
refs: [childrenRef, popupRef], | ||
onClickOutside: handleClose, | ||
}); | ||
|
||
useKeydown({ | ||
listening: visible, | ||
keyCode: 27, | ||
onKeydown: handleClose, | ||
}); | ||
|
||
// TODO: compose events | ||
const renderChildren = ({ ref }: { ref: RefObject<HTMLElement> }) => { | ||
if (children instanceof Function) { | ||
return children({ | ||
ref, | ||
bind: { | ||
onClick: toggle, | ||
}, | ||
}); | ||
} | ||
|
||
if (!isValidElement<any>(children)) { | ||
return children; | ||
} | ||
|
||
return cloneElement(children, { | ||
ref, | ||
onClick: toggle, | ||
}); | ||
}; | ||
|
||
return ( | ||
<Positioner | ||
positionerRef={popupRef} | ||
targetRef={childrenRef} | ||
visible={visible} | ||
position={position} | ||
positioner={({ style }) => ( | ||
<PopoverPopup | ||
ref={popupRef} | ||
style={style} | ||
title={title} | ||
content={content} | ||
handleClose={handleClose} | ||
/> | ||
)} | ||
> | ||
{renderChildren} | ||
</Positioner> | ||
); | ||
}; | ||
|
||
export default Popover; |
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,3 @@ | ||
export { default } from './Popover'; | ||
|
||
export * from './Popover'; |
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,23 @@ | ||
import styled from 'styled-components'; | ||
|
||
export const StyledPopover = styled.div` | ||
border: ${p => p.theme.borders.base}; | ||
border-radius: ${p => p.theme.radii.lg}; | ||
border-color: ${p => p.theme.colors.gray300}; | ||
background-color: ${p => p.theme.colors.light}; | ||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
color: ${p => p.theme.colors.gray700}; | ||
font-size: ${p => p.theme.fontSizes.sm}; | ||
text-align: left; | ||
white-space: nowrap; | ||
`; | ||
|
||
export const PopoverHeader = styled.div` | ||
padding: ${p => p.theme.space[1]} ${p => p.theme.space[2]}; | ||
border-bottom: ${p => p.theme.borders.base}; | ||
border-color: ${p => p.theme.colors.gray300}; | ||
`; | ||
|
||
export const PopoverContent = styled.div` | ||
padding: ${p => p.theme.space[2]}; | ||
`; |
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