Skip to content

Commit

Permalink
feat: allow renderProp pattern in OverlayTrigger (#5316)
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Jul 23, 2020
1 parent 26ee2b9 commit b2bf177
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 100 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -76,8 +76,8 @@
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"prop-types-extra": "^1.1.0",
"react-overlays": "^4.0.0",
"react-transition-group": "^4.0.0",
"react-overlays": "^4.1.0",
"react-transition-group": "^4.4.1",
"uncontrollable": "^7.0.0",
"warning": "^4.0.3"
},
Expand Down
74 changes: 25 additions & 49 deletions src/OverlayTrigger.tsx
Expand Up @@ -5,7 +5,6 @@ import useTimeout from '@restart/hooks/useTimeout';
import safeFindDOMNode from 'react-overlays/safeFindDOMNode';
import warning from 'warning';
import { useUncontrolledProp } from 'uncontrollable';
import { Modifier } from 'react-overlays/esm/usePopper';
import Overlay, { OverlayChildren, OverlayProps } from './Overlay';

export type OverlayTriggerType = 'hover' | 'click' | 'focus';
Expand All @@ -16,9 +15,15 @@ export type OverlayInjectedProps = {
onFocus?: (...args: any[]) => any;
};

export type OverlayTriggerRenderProps = OverlayInjectedProps & {
ref: React.Ref<any>;
};

export interface OverlayTriggerProps
extends Omit<OverlayProps, 'children' | 'target'> {
children: React.ReactElement;
children:
| React.ReactElement
| ((props: OverlayTriggerRenderProps) => React.ReactNode);
trigger?: OverlayTriggerType | OverlayTriggerType[];
delay?: OverlayDelay;
show?: boolean;
Expand Down Expand Up @@ -51,9 +56,9 @@ function normalizeDelay(delay?: OverlayDelay) {
// for cases when the trigger is disabled and mouseOut/Over can cause flicker
// moving from one child element to another.
function handleMouseOverOut(
handler: (...args: any[]) => any,
handler: (...args: [React.MouseEvent, ...any[]]) => any,
args: [React.MouseEvent, ...any[]],
relatedNative,
relatedNative: 'fromElement' | 'toElement',
) {
const [e] = args;
const target = e.currentTarget;
Expand Down Expand Up @@ -188,9 +193,10 @@ function OverlayTrigger({

const delay = normalizeDelay(propsDelay);

const child = React.Children.only(children);

const { onFocus, onBlur, onClick } = child.props;
const { onFocus, onBlur, onClick } =
typeof children !== 'function'
? React.Children.only(children).props
: ({} as any);

const getTarget = useCallback(
() => safeFindDOMNode(triggerNodeRef.current),
Expand Down Expand Up @@ -228,15 +234,15 @@ function OverlayTrigger({
const handleFocus = useCallback(
(...args: any[]) => {
handleShow();
if (onFocus) onFocus(...args);
onFocus?.(...args);
},
[handleShow, onFocus],
);

const handleBlur = useCallback(
(...args: any[]) => {
handleHide();
if (onBlur) onBlur(...args);
onBlur?.(...args);
},
[handleHide, onBlur],
);
Expand All @@ -263,34 +269,6 @@ function OverlayTrigger({
[handleHide],
);

// We add aria-describedby in the case where the overlay is a role="tooltip"
// for other cases describedby isn't appropriate (e.g. a popover with inputs) so we don't add it.
const ariaModifier: Modifier<'ariaDescribedBy', Record<string, unknown>> = {
name: 'ariaDescribedBy',
enabled: true,
phase: 'afterWrite',
effect: ({ state }) => {
return () => {
if ('removeAttribute' in state.elements.reference)
state.elements.reference.removeAttribute('aria-describedby');
};
},
fn: ({ state }) => {
const { popper, reference } = state.elements;

if (!show || !reference) return;

const role = popper.getAttribute('role') || '';
if (
popper.id &&
role.toLowerCase() === 'tooltip' &&
'setAttribute' in reference
) {
reference.setAttribute('aria-describedby', popper.id);
}
},
};

const triggers: string[] = trigger == null ? [] : [].concat(trigger as any);
const triggerProps: any = {};

Expand All @@ -312,25 +290,23 @@ function OverlayTrigger({
triggerProps.onMouseOut = handleMouseOut;
}

// TODO: fix typing
// @ts-ignore
const modifiers = [ariaModifier].concat(popperConfig.modifiers || []);
return (
<>
<RefHolder ref={triggerNodeRef}>
{cloneElement(child as any, triggerProps)}
</RefHolder>
{typeof children === 'function' ? (
children({ ...triggerProps, ref: triggerNodeRef })
) : (
<RefHolder ref={triggerNodeRef}>
{cloneElement(children as any, triggerProps)}
</RefHolder>
)}
<Overlay
{...props}
popperConfig={{
...popperConfig,
modifiers,
}}
show={show}
onHide={handleHide}
target={getTarget as any}
placement={placement}
flip={flip}
placement={placement}
popperConfig={popperConfig}
target={getTarget as any}
>
{overlay}
</Overlay>
Expand Down
15 changes: 2 additions & 13 deletions www/src/components/Heading.js
Expand Up @@ -10,18 +10,7 @@ const styles = css`
composes: __heading from global;
position: relative;
pointer-events: none;
&:before {
display: block;
height: 6rem;
margin-top: -6rem;
visibility: hidden;
content: '';
}
}
.inner {
pointer-events: auto;
scroll-margin-top: 5rem;
}
`;

Expand All @@ -35,7 +24,7 @@ const Heading = ({ h, id, title, className, children, registerNode }) => {
const H = `h${h}`;
return (
<H id={id} className={classNames(className, styles.heading)}>
<div className={styles.inner}>{children}</div>
{children}
</H>
);
};
Expand Down
4 changes: 1 addition & 3 deletions www/src/components/NavMain.js
Expand Up @@ -32,8 +32,6 @@ const Banner = styled(Navbar).attrs({
}
@include media-breakpoint-up(md) {
position: sticky;
top: 0rem;
z-index: 1040;
}
`;
Expand All @@ -51,7 +49,7 @@ const StyledNavbar = styled(Navbar).attrs({
@include media-breakpoint-up(md) {
position: sticky;
top: 4rem;
top: 0rem;
z-index: 1040;
}
`;
Expand Down
6 changes: 4 additions & 2 deletions www/src/components/SideNav.js
Expand Up @@ -18,15 +18,17 @@ const MenuButton = styled(Button).attrs({ variant: 'link' })`
const SidePanel = styled('div')`
@import '../css/theme';
$top: 4rem;
composes: d-flex flex-column from global;
background-color: #f7f7f7;
@include media-breakpoint-up(md) {
position: sticky;
top: 4rem;
top: $top;
z-index: 1000;
height: calc(100vh - 4rem);
height: calc(100vh - #{$top});
background-color: #f7f7f7;
border-right: 1px solid $divider;
}
Expand Down
6 changes: 4 additions & 2 deletions www/src/components/Toc.js
Expand Up @@ -6,10 +6,12 @@ export const TocContext = React.createContext();
const SidePanel = styled('div')`
@import '../css/theme';
$top: 4rem;
order: 2;
position: sticky;
top: 4rem;
height: calc(100vh - 4rem);
top: $top;
height: calc(100vh - #{$top});
padding-top: 1.5rem;
padding-bottom: 1.5rem;
font-size: 0.875rem;
Expand Down
19 changes: 0 additions & 19 deletions www/src/examples/Overlays/OverlayTrigger.js

This file was deleted.

15 changes: 15 additions & 0 deletions www/src/examples/Overlays/Trigger.js
@@ -0,0 +1,15 @@
const renderTooltip = (props) => (
<Tooltip id="button-tooltip" {...props}>
Simple tooltip
</Tooltip>
);

render(
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip}
>
<Button variant="success">Hover me to see</Button>
</OverlayTrigger>,
);
21 changes: 21 additions & 0 deletions www/src/examples/Overlays/TriggerRenderProp.js
@@ -0,0 +1,21 @@
render(
<OverlayTrigger
placement="bottom"
overlay={<Tooltip id="button-tooltip-2">Check out this avatar</Tooltip>}
>
{({ ref, ...triggerHandler }) => (
<Button
variant="light"
{...triggerHandler}
className="d-inline-flex align-items-center"
>
<Image
ref={ref}
roundedCircle
src="holder.js/20x20?text=J&bg=28a745&fg=FFF"
/>
<span className="ml-1">Hover to see</span>
</Button>
)}
</OverlayTrigger>,
);
27 changes: 25 additions & 2 deletions www/src/pages/components/overlays.js
Expand Up @@ -5,10 +5,12 @@ import { css } from 'astroturf';
import LinkedHeading from '../../components/LinkedHeading';
import ComponentApi from '../../components/ComponentApi';
import ReactPlayground from '../../components/ReactPlayground';
import Callout from '../../components/Callout';

import Disabled from '../../examples/Overlays/Disabled';
import Overlay from '../../examples/Overlays/Overlay';
import OverlayTrigger from '../../examples/Overlays/OverlayTrigger';
import OverlayTrigger from '../../examples/Overlays/Trigger';
import TriggerRenderProp from '../../examples/Overlays/TriggerRenderProp';
import PopoverBasic from '../../examples/Overlays/PopoverBasic';
import PopoverContained from '../../examples/Overlays/PopoverContained';
import PopoverPositioned from '../../examples/Overlays/PopoverPositioned';
Expand Down Expand Up @@ -91,7 +93,7 @@ export default withLayout(function TooltipSection({ data }) {
</p>
<ReactPlayground codeText={Overlay} />

<LinkedHeading h="3" id="overlay-trigger">
<LinkedHeading h="2" id="overlay-trigger">
OverlayTrigger
</LinkedHeading>
<p>
Expand All @@ -116,6 +118,27 @@ export default withLayout(function TooltipSection({ data }) {

<ReactPlayground codeText={OverlayTrigger} />

<LinkedHeading h="3" id="customizing-trigger-behavior">
Customizing trigger behavior
</LinkedHeading>

<p>
For more advanced behaviors <code>{'<OverlayTrigger>'}</code> accepts a
function child that passes in the injected <code>ref</code> and event
handlers that coorespond to the configured <code>trigger</code> prop.
</p>
<p>
You can manually apply the props to any element you want or split them
up. The example below shows how to position the overlay to a different
element than the one that triggers its visibility.
</p>
<Callout>
<strong>Pro Tip:</strong> Using the function form of OverlayTrigger
avoids a <code>React.findDOMNode</code> call, for those trying to be
strict mode compliant.
</Callout>
<ReactPlayground codeText={TriggerRenderProp} />

<LinkedHeading h="2" id="tooltips">
Tooltips
</LinkedHeading>
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Expand Up @@ -7601,10 +7601,10 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==

react-overlays@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-4.0.0.tgz#7fbcb60d12fee3733e9e4dd216b225e94fb3befe"
integrity sha512-LpznWocwgeB5oWKg6cDdkqKP7MbX4ClKbJqgZGUMXPRBBYcqrgM6TjjZ/8DeurNU//GuqwQMjhmo/JVma4XEWw==
react-overlays@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-4.1.0.tgz#755a890519b02e3904845172d5223ff2dfb1bb29"
integrity sha512-vdRpnKe0ckWOOD9uWdqykLUPHLPndIiUV7XfEKsi5008xiyHCfL8bxsx4LbMrfnxW1LzRthLyfy50XYRFNQqqw==
dependencies:
"@babel/runtime" "^7.4.5"
"@popperjs/core" "^2.0.0"
Expand All @@ -7625,10 +7625,10 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1:
react-is "^16.8.6"
scheduler "^0.19.1"

react-transition-group@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
Expand Down

1 comment on commit b2bf177

@kartikbhalla12
Copy link

@kartikbhalla12 kartikbhalla12 commented on b2bf177 Jul 23, 2020

Choose a reason for hiding this comment

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

This transition warning is also present with bootstrap-carousel i guess😅😅

Please sign in to comment.