diff --git a/docs/lab/Popover.mdx b/docs/lab/Popover.mdx new file mode 100644 index 00000000..d172d879 --- /dev/null +++ b/docs/lab/Popover.mdx @@ -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 + + + + + + + +### With position right + + + + + + + +### With content hide + + + ( +
+ Popover Content +
+ +
+ )} + > + +
+
+ +### With onVisibleChange + + + + + + + +## API + + diff --git a/packages/tailor-ui-lab/src/Popover/Popover.tsx b/packages/tailor-ui-lab/src/Popover/Popover.tsx new file mode 100644 index 00000000..aed7d597 --- /dev/null +++ b/packages/tailor-ui-lab/src/Popover/Popover.tsx @@ -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( + function PopoverPopup( + { style, title, content, handleClose, ...otherProps }, + ref + ) { + return ( + + + {title && ( + + + {title instanceof Function ? title(handleClose) : title} + + + )} + + {content instanceof Function ? content(handleClose) : content} + + + + ); + } +); + +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 = ({ + 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 }) => { + if (children instanceof Function) { + return children({ + ref, + bind: { + onClick: toggle, + }, + }); + } + + if (!isValidElement(children)) { + return children; + } + + return cloneElement(children, { + ref, + onClick: toggle, + }); + }; + + return ( + ( + + )} + > + {renderChildren} + + ); +}; + +export default Popover; diff --git a/packages/tailor-ui-lab/src/Popover/index.ts b/packages/tailor-ui-lab/src/Popover/index.ts new file mode 100644 index 00000000..0ac9edb9 --- /dev/null +++ b/packages/tailor-ui-lab/src/Popover/index.ts @@ -0,0 +1,3 @@ +export { default } from './Popover'; + +export * from './Popover'; diff --git a/packages/tailor-ui-lab/src/Popover/styles.tsx b/packages/tailor-ui-lab/src/Popover/styles.tsx new file mode 100644 index 00000000..bea7fb82 --- /dev/null +++ b/packages/tailor-ui-lab/src/Popover/styles.tsx @@ -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]}; +`; diff --git a/packages/tailor-ui-lab/src/Tooltip/Tooltip.tsx b/packages/tailor-ui-lab/src/Tooltip/Tooltip.tsx index 98689129..9faebe64 100644 --- a/packages/tailor-ui-lab/src/Tooltip/Tooltip.tsx +++ b/packages/tailor-ui-lab/src/Tooltip/Tooltip.tsx @@ -91,14 +91,16 @@ const Tooltip: FunctionComponent = ({ const cancelLeaveDebounce = useRef void)>(); const [visibleFromSelf, setVisibleFromSelf] = useState(defaultVisible); - const hanVisibleFromProps = typeof visibleFromProps !== 'undefined'; + const hasVisibleFromProps = typeof visibleFromProps !== 'undefined'; - const visible = hanVisibleFromProps ? visibleFromProps : visibleFromSelf; + const visible = hasVisibleFromProps ? visibleFromProps : visibleFromSelf; const open = () => { if (onVisibleChange) { onVisibleChange(true); - } else { + } + + if (!hasVisibleFromProps) { setVisibleFromSelf(true); } }; @@ -106,7 +108,9 @@ const Tooltip: FunctionComponent = ({ const close = () => { if (onVisibleChange) { onVisibleChange(false); - } else { + } + + if (!hasVisibleFromProps) { setVisibleFromSelf(false); } }; diff --git a/packages/tailor-ui-lab/src/index.ts b/packages/tailor-ui-lab/src/index.ts index d32ab47d..b64c1d06 100644 --- a/packages/tailor-ui-lab/src/index.ts +++ b/packages/tailor-ui-lab/src/index.ts @@ -5,3 +5,4 @@ export { default as Tabs } from './Tabs'; export * from './Tabs'; export { default as Tooltip } from './Tooltip'; +export { default as Popover } from './Popover';