Skip to content

Commit

Permalink
fix(Popover): support bind ref with children (#1473)
Browse files Browse the repository at this point in the history
* fix(Popover): support bind ref with children

* fix(Dropdown): fix type issue

* fix(Popover): remove inject classname

* fix(Popover): fix lint issue

Co-authored-by: Zhang Rui <zhangrui@growingio.com>
  • Loading branch information
Ryan Zhang and Zhang Rui committed Nov 16, 2021
1 parent e19fe2c commit d56229a
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 31 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"rc-virtual-list": "^3.2.6",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^14.0.0",
"react-is": "^17.0.2",
"react-lines-ellipsis": "^0.15.0",
"react-popper": "^2.2.5",
"react-resizable": "^3.0.4",
Expand Down
4 changes: 2 additions & 2 deletions src/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export function Dropdown<T = HTMLElement>(props: DropdownProps, ref: React.Forwa

const getDropdownTrigger = () => {
const child = Children.only(children);
return cloneElement(child, {
return cloneElement(child as React.ReactElement, {
className: classnames(
{
'dropdown-active': controlledVisible,
},
child.props.className,
(child as React.ReactElement).props.className,
ref
),
});
Expand Down
85 changes: 67 additions & 18 deletions src/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { usePopper } from 'react-popper';
import ReactDOM from 'react-dom';
import { PopoverProps, placements } from './interface';
import usePrefixCls from '../utils/hooks/use-prefix-cls';
import { composeRef, supportRef } from '../utils/composeRef';

const Popover = (props: PopoverProps) => {
const {
Expand Down Expand Up @@ -47,6 +48,16 @@ const Popover = (props: PopoverProps) => {
[prefixCls, overlayClassName, visible, allowArrow]
);

const triggerChildEvent = useCallback(
(name: string, e: any) => {
if (supportRef(children)) {
const fireEvent = (children as React.ReactElement)?.props?.[name];
fireEvent?.(e);
}
},
[children]
);

const contentInnerCls = useMemo(
() => classNames(overlayInnerClassName, `${prefixCls}__content-inner`),
[prefixCls, overlayInnerClassName]
Expand Down Expand Up @@ -98,25 +109,45 @@ const Popover = (props: PopoverProps) => {
const isFocusToShow = useMemo(() => trigger.indexOf('focus') !== -1, [trigger]);

const onMouseEnter = useMemo(
() => debounce(() => isHoverToShow && updateVisible(true), 100),
[isHoverToShow, updateVisible]
() =>
debounce((e: Event) => {
triggerChildEvent('onMouseEnter', e);
isHoverToShow && updateVisible(true);
}, 100),
[triggerChildEvent, isHoverToShow, updateVisible]
);
const onMouseLeave = useMemo(
() => debounce(() => isHoverToShow && updateVisible(false), 100),
[isHoverToShow, updateVisible]
() =>
debounce((e: Event) => {
triggerChildEvent('onMouseLeave', e);
isHoverToShow && updateVisible(false);
}, 100),
[triggerChildEvent, isHoverToShow, updateVisible]
);

const onClick = useCallback(() => {
if (!isHoverToShow && !isFocusToShow) {
isClickToShow && updateVisible(!visible);
}
}, [isClickToShow, isHoverToShow, isFocusToShow, visible, updateVisible]);
const onFocus = useCallback(() => {
isFocusToShow && updateVisible(true);
}, [isFocusToShow, updateVisible]);
const onBlur = useCallback(() => {
isFocusToShow && updateVisible(false);
}, [isFocusToShow, updateVisible]);
const onClick = useCallback(
(e: Event) => {
triggerChildEvent('onClick', e);
if (!isHoverToShow && !isFocusToShow) {
isClickToShow && updateVisible(!visible);
}
},
[triggerChildEvent, isHoverToShow, isFocusToShow, isClickToShow, updateVisible, visible]
);
const onFocus = useCallback(
(e: Event) => {
triggerChildEvent('onFocus', e);
isFocusToShow && updateVisible(true);
},
[triggerChildEvent, isFocusToShow, updateVisible]
);
const onBlur = useCallback(
(e: Event) => {
triggerChildEvent('onBlur', e);
isFocusToShow && updateVisible(false);
},
[triggerChildEvent, isFocusToShow, updateVisible]
);

const onContentMouseEnter = useCallback(() => {
overContentRef.current = true;
Expand Down Expand Up @@ -163,11 +194,29 @@ const Popover = (props: PopoverProps) => {
</div>
</div>
);

// =============== refs =====================
let triggerNode = (
<div className={`${prefixCls}__popcorn`} ref={referenceElement} {...divRoles}>
{children}
</div>
);

if (supportRef(children)) {
const cloneProps = {
...divRoles,
className: classNames((children as React.ReactElement)?.props?.className, overlayClassName),
style: { ...(children as React.ReactElement)?.props?.style, ...overlayInnerStyle },
};

const child = React.Children.only(children);
cloneProps.ref = composeRef(referenceElement, (child as any).ref);
triggerNode = React.cloneElement(child as React.ReactElement, cloneProps);
}

return (
<>
<div className={`${prefixCls}__popcorn`} ref={referenceElement} {...divRoles}>
{children}
</div>
{triggerNode}
{typeof getContainer === 'function'
? ReactDOM.createPortal(contentRender, getContainer(referenceElement.current as HTMLDivElement))
: contentRender}
Expand Down
36 changes: 36 additions & 0 deletions src/popover/demos/Popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,39 @@ export const Portal = PortalTemplate.bind({});
Portal.args = {
content,
};

const SupportRefTemplate: Story<PopoverProps> = (args) => (
<Popover {...args} strategy="fixed">
Only String
</Popover>
);

export const SupportRef = SupportRefTemplate.bind({});
SupportRef.args = {
content,
};

const NotSupportRefTemplate: Story<PopoverProps> = (args) => {
const onClick = () => {
console.log('Click trigger button!');
};
return (
<>
<span>margin: 30px</span>
<br />
<div style={{ border: '1px solid #3c3c3c', borderRadius: '4px', display: 'inline-block' }}>
<Popover {...args} strategy="fixed">
<Button style={{ margin: 30 }} onClick={onClick}>
Button Trigger
</Button>
</Popover>
</div>
</>
);
};

export const NotSupportRef = NotSupportRefTemplate.bind({});
NotSupportRef.args = {
trigger: 'click',
content,
};
2 changes: 1 addition & 1 deletion src/popover/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface PopoverProps {
/**
被包裹的元素
*/
children: React.ReactElement;
children: React.ReactElement | string;

placement?: Placement;
trigger?: TriggerAction | TriggerAction[];
Expand Down
3 changes: 3 additions & 0 deletions src/popover/style/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
@popover-background-color: #ffffff00;

.@{popover-prefix-cls} {
&__popcorn {
display: inline-block;
}
&__content {
z-index: @z-index-popover;
visibility: hidden;
Expand Down
1 change: 1 addition & 0 deletions src/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ declare module '*.md';
declare module '*.mdx';
declare module 'react-lines-ellipsis';
declare module 'react-lines-ellipsis/lib/responsiveHOC';
declare module 'react-is';
41 changes: 31 additions & 10 deletions src/utils/composeRef.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { isMemo } from 'react-is';

const composeRef = <T>(...refs: React.Ref<T>[]): React.Ref<T> => (node: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
// eslint-disable-next-line no-param-reassign
(ref as any).current = node;
}
});
};
export const composeRef =
<T>(...refs: React.Ref<T>[]): React.Ref<T> =>
(node: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
// eslint-disable-next-line no-param-reassign
(ref as any).current = node;
}
});
};

export function supportRef(nodeOrComponent: any): boolean {
if (typeof nodeOrComponent !== 'object') {
return false;
}
const type = isMemo(nodeOrComponent) ? nodeOrComponent.type.type : nodeOrComponent.type;

// Function component node
if (typeof type === 'function' && !type.prototype?.render) {
return false;
}

// Class component
if (typeof nodeOrComponent === 'function' && !nodeOrComponent.prototype?.render) {
return false;
}

return true;
}
export default composeRef;

1 comment on commit d56229a

@vercel
Copy link

@vercel vercel bot commented on d56229a Nov 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.