Skip to content

Commit

Permalink
feat(popover): implement new popover with positioner
Browse files Browse the repository at this point in the history
  • Loading branch information
jigsawye committed Mar 26, 2019
1 parent 07bc232 commit c86e711
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 4 deletions.
82 changes: 82 additions & 0 deletions docs/lab/Popover.mdx
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} />
181 changes: 181 additions & 0 deletions packages/tailor-ui-lab/src/Popover/Popover.tsx
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;
3 changes: 3 additions & 0 deletions packages/tailor-ui-lab/src/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './Popover';

export * from './Popover';
23 changes: 23 additions & 0 deletions packages/tailor-ui-lab/src/Popover/styles.tsx
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]};
`;
12 changes: 8 additions & 4 deletions packages/tailor-ui-lab/src/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,26 @@ const Tooltip: FunctionComponent<TooltipProps> = ({
const cancelLeaveDebounce = useRef<null | (() => 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);
}
};

const close = () => {
if (onVisibleChange) {
onVisibleChange(false);
} else {
}

if (!hasVisibleFromProps) {
setVisibleFromSelf(false);
}
};
Expand Down
1 change: 1 addition & 0 deletions packages/tailor-ui-lab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

0 comments on commit c86e711

Please sign in to comment.