Skip to content

Commit

Permalink
feat(tooltip, popover): add popover component, add arrowPointAtCenter… (
Browse files Browse the repository at this point in the history
#33)

affects: @gio-design/components, @gio-design/tokens, website

* style: fix popover style issue cause by ghost blank node. and update doc example
* docs: create group for functional components

Co-authored-by: lihang <lihang@growingio.com>
  • Loading branch information
LEEHONCN and lihang authored Jul 24, 2020
1 parent 754e044 commit 3921949
Show file tree
Hide file tree
Showing 36 changed files with 729 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import Popover from '../index';
import '@gio-design/components/es/components/Tabs/style/index.css';
import { act } from 'react-dom/test-utils';
import { mount, render } from 'enzyme';

async function waitForComponentToPaint(wrapper, amount = 500) {
await act(async () => new Promise((resolve) => setTimeout(resolve, amount)).then(() => wrapper.update()));
}

describe('Testing Popover', () => {
const getPopover = () => (
<Popover contentArea='content' footerArea='footer'>
<span>Test</span>
</Popover>
);

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('should be stable', () => {
const wrapper = render(getPopover());
expect(wrapper).toMatchSnapshot();
});

it('should be mount, setProps, unmount with no error', () => {
expect(() => {
const wrapper = mount(getPopover());
wrapper.setProps({ contentArea: 'content update' });
wrapper.setProps({ visible: 'true' });
wrapper.unmount();
}).not.toThrow();
});

test('prop contentArea', () => {
const wrapper = mount(getPopover());
wrapper.setProps({ contentArea: 'new text', footerArea: null });
wrapper.setProps({ trigger: 'click' });
wrapper.find('span').at(0).simulate('click');
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-content')).toBe(true);
expect(wrapper.find('.gio-popover-inner-content').text()).toBe('new text');
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-footer')).toBe(false);
});

test('prop footerArea', () => {
const wrapper = mount(getPopover());
wrapper.setProps({ trigger: 'click' });
wrapper.find('span').at(0).simulate('click');
wrapper.setProps({ contentArea: 0, footerArea: 'only footer' });
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-footer')).toBe(true);
expect(wrapper.find('.gio-popover-inner-footer').text()).toBe('only footer');
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-content')).toBe(false);
});

it('should be render rightly', () => {
const wrapper = mount(getPopover());
wrapper.setProps({ trigger: 'click' });
wrapper.setProps({ placement: 'topLeft' });
wrapper.setProps({ overlayClassName: 'overlayClassName' });
wrapper.find('span').at(0).simulate('click');
expect(wrapper.exists('.gio-popover-inner')).toBe(true);
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-content')).toBe(true);
expect(wrapper.find('.gio-popover-inner').exists('.gio-popover-inner-footer')).toBe(true);
expect(wrapper.exists('.overlayClassName')).toBe(true);
waitForComponentToPaint(wrapper).then(() => {
expect(wrapper.exists('.gio-popover-placement-topLeft')).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing Popover should be stable 1`] = `
initialize {
"0": Object {
"attribs": Object {},
"children": Array [
Object {
"data": "Test",
"next": null,
"parent": [Circular],
"prev": null,
"type": "text",
},
],
"name": "span",
"namespace": "http://www.w3.org/1999/xhtml",
"next": null,
"parent": null,
"prev": null,
"root": Object {
"attribs": Object {},
"children": Array [
[Circular],
],
"name": "root",
"namespace": "http://www.w3.org/1999/xhtml",
"next": null,
"parent": null,
"prev": null,
"type": "root",
"x-attribsNamespace": Object {},
"x-attribsPrefix": Object {},
},
"type": "tag",
"x-attribsNamespace": Object {},
"x-attribsPrefix": Object {},
},
"_root": [Circular],
"length": 1,
"options": Object {
"decodeEntities": true,
"normalizeWhitespace": false,
"withDomLvl1": true,
"xml": false,
},
}
`;
24 changes: 24 additions & 0 deletions packages/components/src/components/popover/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { useContext } from 'react';
import Tooltip from '../tooltip';
import { PopoverProps } from './interface';
import { ConfigContext } from '../config-provider';

const Popover: React.FC<PopoverProps> = (props: PopoverProps) => {
const { children, contentArea, footerArea, prefixCls: customizePrefixCls, ...rest } = props;
const { getPrefixCls } = useContext(ConfigContext);
const prefixCls = getPrefixCls('popover', customizePrefixCls);

const popoverOverlay = () => (
<>
{contentArea && <div className={`${prefixCls}-inner-content`}>{contentArea}</div>}
{footerArea && <div className={`${prefixCls}-inner-footer`}>{footerArea}</div>}
</>
);
return (
<Tooltip prefixCls={prefixCls} overlay={popoverOverlay()} {...rest}>
{children}
</Tooltip>
);
};

export default Popover;
7 changes: 7 additions & 0 deletions packages/components/src/components/popover/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TooltipProps } from '../tooltip/interface';

export type ReactRender = () => React.ReactNode;
export interface PopoverProps extends Omit<TooltipProps, 'title' | 'tooltipLink'> {
contentArea: React.ReactNode | ReactRender;
footerArea?: React.ReactNode | ReactRender;
}
64 changes: 64 additions & 0 deletions packages/components/src/components/popover/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
@import '../../../stylesheet/theme.less';
@import '../../../stylesheet/mixin/trigger.less';
@import '~@gio-design/tokens/dist/variables.less';

@popover-prefix-cls: ~'@{component-prefix}-popover';
@popover-arrow-width: 12px;
@distance: 6px;
@popover-offset: 20px;
@popover-duration: 10ms;

.@{popover-prefix-cls}{
margin: auto;
position: absolute;
z-index: 100;

&-hidden {
display: none;
}

&-content {
position: relative;
}

&-inner {
box-shadow: @shadow-popover;
border-radius: 4px;
background-color: @color-background-popover;
display: block;
overflow: hidden;
&-content{
margin: 20px;
position: relative;
overflow: hidden;
}
&-footer {
margin: -4px 20px 16px;
position: relative;
overflow: hidden;
}
}

&-arrow {
position: absolute;
display: block;
pointer-events: none;
height: 12px;
width: 12px;
&-content {
position: absolute;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 0px;
height: 0px;
border: @popover-arrow-width/2 solid;
pointer-events: auto;
}
}
.trigger-placement(@popover-prefix-cls, @color-background-popover, @popover-arrow-width, @distance, @popover-offset);
}

.trigger-transition(@popover-prefix-cls, @popover-duration);
16 changes: 13 additions & 3 deletions packages/components/src/components/tooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RcTooltip from 'rc-tooltip';
import { TooltipProps } from './interface';
import { ConfigContext } from '../config-provider';
import Link from '../link';
import getPlacements from './placements';

const Tooltip = (props: TooltipProps) => {
const {
Expand All @@ -13,12 +14,13 @@ const Tooltip = (props: TooltipProps) => {
prefixCls: customizePrefixCls,
overlay,
children,
arrowPointAtCenter,
...rest
} = props;
const { getPrefixCls } = useContext(ConfigContext);
const prefixCls = getPrefixCls('tooltip', customizePrefixCls);

const defaultOverlay = () => (
const tooltipOverlay = () => (
<>
<span className={`${prefixCls}-inner-title`}>{title}</span>
{tooltipLink?.link && (
Expand All @@ -29,7 +31,14 @@ const Tooltip = (props: TooltipProps) => {
</>
);

const getOverlay = () => overlay || defaultOverlay();
const setCursor = (child: React.ReactElement) => {
if (trigger === 'click' || (Array.isArray(trigger) && trigger.includes('click'))) {
return React.cloneElement(child, { style: { cursor: 'pointer' } });
}
return child;
};

const getOverlay = () => overlay || tooltipOverlay();

return (
<RcTooltip
Expand All @@ -39,9 +48,10 @@ const Tooltip = (props: TooltipProps) => {
transitionName='spread-transition'
arrowContent={<span className={`${prefixCls}-arrow-content`} />}
overlay={getOverlay()}
builtinPlacements={getPlacements({ arrowPointAtCenter })}
{...rest}
>
{children}
{setCursor(children)}
</RcTooltip>
);
};
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/tooltip/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export interface TooltipLink {
export interface TooltipProps extends Partial<RcTooltipProps> {
title?: React.ReactNode | ReactRender;
tooltipLink?: TooltipLink;
children?: React.ReactElement;
placement?: string;
arrowPointAtCenter?: boolean;
children: React.ReactElement;
}
112 changes: 112 additions & 0 deletions packages/components/src/components/tooltip/placements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { placements } from 'rc-tooltip/lib/placements';
import { BuildInPlacements } from 'rc-trigger';

const autoAdjustOverflowEnabled = {
adjustX: 1,
adjustY: 1,
};

const autoAdjustOverflowDisabled = {
adjustX: 0,
adjustY: 0,
};

const targetOffset = [0, 0];

export interface AdjustOverflow {
adjustX?: 0 | 1;
adjustY?: 0 | 1;
}

export interface PlacementsConfig {
arrowWidth?: number;
horizontalArrowShift?: number;
verticalArrowShift?: number;
arrowPointAtCenter?: boolean;
autoAdjustOverflow?: boolean | AdjustOverflow;
}

export function getOverflowOptions(autoAdjustOverflow?: boolean | AdjustOverflow) {
if (typeof autoAdjustOverflow === 'boolean') {
return autoAdjustOverflow ? autoAdjustOverflowEnabled : autoAdjustOverflowDisabled;
}
return {
...autoAdjustOverflowDisabled,
...autoAdjustOverflow,
};
}

export default function getPlacements(config: PlacementsConfig) {
const {
arrowWidth = 6,
horizontalArrowShift = 20,
verticalArrowShift = 20,
autoAdjustOverflow = true,
arrowPointAtCenter,
} = config;
const placementMap: BuildInPlacements = {
left: {
points: ['cr', 'cl'],
offset: [-4, 0],
},
right: {
points: ['cl', 'cr'],
offset: [4, 0],
},
top: {
points: ['bc', 'tc'],
offset: [0, -4],
},
bottom: {
points: ['tc', 'bc'],
offset: [0, 4],
},
topLeft: {
points: ['bl', 'tc'],
offset: [-(horizontalArrowShift + arrowWidth), -4],
},
leftTop: {
points: ['tr', 'cl'],
offset: [-4, -(verticalArrowShift + arrowWidth)],
},
topRight: {
points: ['br', 'tc'],
offset: [horizontalArrowShift + arrowWidth, -4],
},
rightTop: {
points: ['tl', 'cr'],
offset: [4, -(verticalArrowShift + arrowWidth)],
},
bottomRight: {
points: ['tr', 'bc'],
offset: [horizontalArrowShift + arrowWidth, 4],
},
rightBottom: {
points: ['bl', 'cr'],
offset: [4, verticalArrowShift + arrowWidth],
},
bottomLeft: {
points: ['tl', 'bc'],
offset: [-(horizontalArrowShift + arrowWidth), 4],
},
leftBottom: {
points: ['br', 'cl'],
offset: [-4, verticalArrowShift + arrowWidth],
},
};
Object.keys(placementMap).forEach((key) => {
placementMap[key] = arrowPointAtCenter
? {
...placementMap[key],
overflow: getOverflowOptions(autoAdjustOverflow),
targetOffset,
}
: {
...placements[key],
overflow: getOverflowOptions(autoAdjustOverflow),
};

placementMap[key].ignoreShake = true;
});
return placementMap;
}
Loading

0 comments on commit 3921949

Please sign in to comment.