Skip to content

Instantly share code, notes, and snippets.

@blvdmitry
Created April 27, 2019 15:36
Show Gist options
  • Save blvdmitry/c87e29a5ab7988efff6fd91015954680 to your computer and use it in GitHub Desktop.
Save blvdmitry/c87e29a5ab7988efff6fd91015954680 to your computer and use it in GitHub Desktop.
Dialog implementation with hooks
import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from 'components/Button';
import Dialog from 'components/Dialog';
import DialogProvider from 'components/Dialog/DialogProvider';
const Story = (props: { position?: 'right' }) => {
const modalId = 'testModal';
const { show } = Dialog.use(modalId);
return (
<React.Fragment>
<Button text="Show modal" onClick={show} />
<Dialog
position={props.position}
id={modalId}
title="Save template"
subtitle="Set cadence, add topic and editors for future use"
actions={[{ text: 'Create' }]}
>
<div style={{ height: 400, background: '#f3f3f3' }} />
</Dialog>
</React.Fragment>
);
};
storiesOf('Dialog', module)
.add('Default', () => <DialogProvider><Story /></DialogProvider>)
.add('Right', () => <DialogProvider><Story position="right" /></DialogProvider>);
import React from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import useOnClickOutside from 'use-onclickoutside';
import Button from 'components/Button';
import Group from 'components/Group';
import Icon from 'components/Icon';
import context, { useDialog } from './DialogContext';
import * as T from './Dialog.types';
import s from './Dialog.pcss';
const Dialog = (props: T.Props) => {
const { title, subtitle, actions, children, id, position } = props;
const rootClassNames = classnames(s.root, position && s[`--position-${position}`]);
const { setPosition } = React.useContext(context);
const rootRef = React.useRef(null);
const { hide, ref, active } = Dialog.use(id);
React.useEffect(() => {
if (active) setPosition(position || null);
}, [position, active]);
useOnClickOutside(rootRef, hide);
if (!active || !ref || !ref.current) return null;
const result = (
<div className={rootClassNames} ref={rootRef}>
<div className={s.title}>{ title }</div>
{ subtitle && <div className={s.subtitle}>{ subtitle }</div> }
{ children && <div className={s.content}>{ children }</div> }
{
actions && (
<div className={s.actions}>
<Group inline>
{ actions.map(action => <Button {...action} key={action.text} />) }
</Group>
</div>
)
}
<button className={s.close} onClick={hide}>
<Icon name="close" size="medium" />
</button>
</div>
);
return ReactDOM.createPortal(result, ref.current);
};
Dialog.use = useDialog;
export default Dialog;
import React from 'react';
import * as T from './Dialog.types';
const error = 'You\'re using the dialog outside of the DialogProvider';
const context = React.createContext<T.ContextData>({
show: () => console.error(error),
hide: () => console.error(error),
setPosition: () => console.log(error),
activeId: null,
position: null,
ref: null,
});
export const useDialog = (id: string) => {
const { show, hide, activeId, ref } = React.useContext(context);
return {
show: () => show(id),
hide,
ref,
active: activeId === id,
};
};
export default context;
import React from 'react';
import classnames from 'classnames';
import CssTransition from 'react-transition-group/CSSTransition';
import Overlay from 'components/Overlay';
import DialogContext from './DialogContext';
import * as T from './Dialog.types';
import s from './DialogProvider.pcss';
const DialogProvider = (props: T.ProviderProps) => {
const ref = React.useRef(null);
const [activeId, setActiveId] = React.useState<string | null>(null);
const [visible, setVisible] = React.useState(false);
const [position, setPosition] = React.useState<T.Position | null>(null);
const timeout = 300;
const show: T.Show = async (id) => {
await setActiveId(id);
// Visible is triggered after id is set and Dialog defines its position
setVisible(true);
};
const hide = () => {
setVisible(false);
setTimeout(() => setActiveId(null), timeout);
};
const positionClassName = position ? s[`--animator-position-${position}`] : s['--animator-position-center'];
const activeClassName = s['--animator-active'];
const animatedClassName = s['--animator-animated'];
return (
<DialogContext.Provider value={{ hide, show, setPosition, activeId, ref, position }}>
{ props.children }
<Overlay onClose={hide} active={visible}>
<div className={s.root}>
<CssTransition
timeout={timeout}
in={visible}
classNames={{
enter: positionClassName,
enterActive: classnames(positionClassName, activeClassName, animatedClassName),
enterDone: classnames(positionClassName, activeClassName, animatedClassName),
exit: classnames(positionClassName, animatedClassName),
exitActive: classnames(positionClassName, animatedClassName),
exitDone: classnames(positionClassName),
}}
>
<div ref={ref} className={s.animator} />
</CssTransition>
</div>
</Overlay>
</DialogContext.Provider>
);
};
export default DialogProvider;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment