Skip to content

Commit

Permalink
feat(dropdown): add new Dropdown, DropdownToggle, DropdownMenu & Drop…
Browse files Browse the repository at this point in the history
…downItem components
  • Loading branch information
dackmin committed Jun 1, 2020
1 parent 3668b29 commit 6b29874
Show file tree
Hide file tree
Showing 16 changed files with 791 additions and 0 deletions.
21 changes: 21 additions & 0 deletions lib/ColorField/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

import { classNames } from '../utils';

const ColorField = ({
className,
}) => {

return (
<div
className={classNames(
'junipero',
''
)}
>

</div>
);
};

export default ColorField;
186 changes: 186 additions & 0 deletions lib/Dropdown/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useReducer,
useRef,
} from 'react';
import PropTypes from 'prop-types';
import { usePopper } from 'react-popper';

import DropdownToggle from '../DropdownToggle';
import DropdownMenu from '../DropdownMenu';
import { classNames, mockState } from '../utils';
import { DropdownContext } from '../contexts';
import { useEventListener } from '../hooks';

const Dropdown = forwardRef(({
children,
className,
disabled = false,
globalEventsTarget = global,
opened = false,
placement = 'bottom-start',
popperOptions = {},
onToggle = () => {},
...rest
}, ref) => {
const innerRef = useRef();
const toggleRef = useRef();
const menuRef = useRef();
const [state, dispatch] = useReducer(mockState, {
opened,
});
const { styles, attributes, update, forceUpdate } = usePopper(
toggleRef.current || innerRef.current,
menuRef.current || innerRef.current,
{
...popperOptions,
placement,
modifiers: [
...(popperOptions.modifiers || []),
{ name: 'offset', options: { offset: [0, 10] } },
],
}
);

useEffect(() => {
dispatch({ opened: disabled ? false : opened });
}, [opened, disabled]);

useEventListener('click', e => {
onClickOutside_(e);
}, globalEventsTarget);

useImperativeHandle(ref, () => ({
opened: state.opened,
innerRef,
toggleRef,
menuRef,
toggle,
open,
close,
update: update_,
forceUpdate: forceUpdate_,
}));

const open = () => {
if (disabled) {
return;
}

state.opened = true;
onToggle_();
};

const close = () => {
state.opened = false;
onToggle_();
};

const toggle = () => {
state.opened = !state.opened;
onToggle_();
};

/* istanbul ignore next: cannot test popper inside jest */
const update_ = () => {
update?.();
};

/* istanbul ignore next: cannot test popper inside jest */
const forceUpdate_ = () => {
forceUpdate?.();
};

const onToggle_ = () => {
update?.();
dispatch({ opened: state.opened });
onToggle({ opened: state.opened });
};

const onClickOutside_ = e => {
const container = innerRef.current;
const menu = menuRef.current;

if (!container || !menu) {
return;
}

if (
!container.contains(e.target) &&
container !== e.target &&
!menu.contains(e.target) &&
menu !== e.target
) {
close();
}
};

const getContext = () => ({
opened: state.opened,
disabled,
styles,
attributes,
toggle,
update: update_,
forceUpdate: forceUpdate_,
});

return (
<div
className={classNames(
'junipero',
'dropdown',
{
opened: state.opened,
},
className,
)}
ref={innerRef}
{ ...rest }
>
<DropdownContext.Provider value={getContext()}>
{ React.Children.map(children, child => (
child.type === DropdownToggle
? React.cloneElement(child, {
ref: ref_ => {
toggleRef.current = ref_?.innerRef?.current;
if (typeof child?.ref === 'function') {
child.ref(ref_);
} else if (child.ref) {
child.ref.current = ref_;
}
},
})
: child.type === DropdownMenu
? React.cloneElement(child, {
ref: ref_ => {
menuRef.current = ref_?.innerRef?.current;
if (typeof child?.ref === 'function') {
child.ref(ref_);
} else if (child.ref) {
child.ref.current = ref_;
}
},
})
: child
)) }
</DropdownContext.Provider>
</div>
);
});

Dropdown.propTypes = {
disabled: PropTypes.bool,
globalEventsTarget: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
]),
opened: PropTypes.bool,
placement: PropTypes.string,
popperOptions: PropTypes.object,
onToggle: PropTypes.func,
};

export default Dropdown;
59 changes: 59 additions & 0 deletions lib/Dropdown/index.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { CSSTransition } from 'react-transition-group';

import Button from '../Button';
import Dropdown from './index';
import DropdownToggle from '../DropdownToggle';
import DropdownMenu from '../DropdownMenu';
import DropdownItem from '../DropdownItem';

export default { title: 'Dropdown' };

export const basic = () => (
<Dropdown onToggle={action('toggle')}>
<DropdownToggle>Click me</DropdownToggle>
<DropdownMenu>
<DropdownItem><a>Menu item 1</a></DropdownItem>
<DropdownItem><a>Menu item 2</a></DropdownItem>
<DropdownItem><a>Menu item 3</a></DropdownItem>
</DropdownMenu>
</Dropdown>
);

export const withButton = () => (
<Dropdown onToggle={action('toggle')}>
<DropdownToggle>
<Button className="primary">Click me</Button>
</DropdownToggle>
<DropdownMenu>
<DropdownItem><a>Menu item 1</a></DropdownItem>
<DropdownItem><a>Menu item 2</a></DropdownItem>
<DropdownItem><a>Menu item 3</a></DropdownItem>
</DropdownMenu>
</Dropdown>
);

export const animated = () => (
<Dropdown
onToggle={action('toggle')}
>
<DropdownToggle>Click to animate me</DropdownToggle>
<DropdownMenu
animate={(menu, { opened }) => (
<CSSTransition
in={opened}
mountOnEnter={true}
unmountOnExit={true}
timeout={300}
classNames="slide-in-up-dropdown"
children={menu}
/>
)}
>
<DropdownItem><a>Menu item 1</a></DropdownItem>
<DropdownItem><a>Menu item 2</a></DropdownItem>
<DropdownItem><a>Menu item 3</a></DropdownItem>
</DropdownMenu>
</Dropdown>
);
3 changes: 3 additions & 0 deletions lib/Dropdown/index.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.junipero.dropdown
position: relative
display: inline-block
Loading

0 comments on commit 6b29874

Please sign in to comment.