Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(OverlayTrigger): allow renderProp pattern #5316

Merged
merged 1 commit into from Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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