Skip to content

Instantly share code, notes, and snippets.

@riccoski
Last active February 17, 2019 23:17
Show Gist options
  • Save riccoski/eb5f6a73e565b53ba286e3f41ff1d679 to your computer and use it in GitHub Desktop.
Save riccoski/eb5f6a73e565b53ba286e3f41ff1d679 to your computer and use it in GitHub Desktop.
Abstracted out the event listener as a custom hook because it shouldn't need to belong in the component and now it's reusable!
// @ts-check
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { useClickOutside, useModal as useToggle } from '../../hooks';
import styles from './Dropdown.module.scss';
function DropdownItem({ onSelect, selected, text, value }) {
function handleClick() {
onSelect(value);
}
return (
<li
role="option"
aria-selected={selected}
className={classNames(styles.item, {
[styles.active]: selected
})}
onClick={handleClick}
>
{text}
</li>
);
}
/**
*
* @param {{ name: String?, onChange: Function?, options: any[], placeholder: String?, style: any}} props
*/
function Dropdown({
name,
onChange,
options,
placeholder = 'Click to select',
style
}) {
const [selected, setSelected] = useState(undefined);
const [open, handleOpen, handleClose] = useToggle();
const [ref] = useClickOutside(handleClose);
const activeOption = options.find(opt => opt.value === selected);
useEffect(() => {
if (onChange) {
onChange(null, {
name,
value: selected
});
}
}, [selected]);
/**
*
* @param {String|Number} selected
*/
function handleSelect(selected) {
setSelected(selected);
handleClose();
}
return (
<div ref={ref} className={styles.dropdown} style={style}>
<div className={styles.container}>
<p onClick={handleOpen} className={styles.button}>
{activeOption ? activeOption.text : placeholder}
</p>
<ul
className={classNames(styles.menu, {
[styles.isOpen]: open
})}
>
{options.map((option, index) => (
<DropdownItem
key={option.key}
selected={selected ? selected === option.value : index === 0}
onSelect={handleSelect}
value={option.value}
text={option.text}
/>
))}
</ul>
</div>
</div>
);
}
export default Dropdown;
import React, { Component } from 'react';
import classNames from 'classnames';
import styles from './Dropdown.module.scss';
export default class Dropdown extends Component {
state = {
open: false,
selectedValue: null
};
ref = null;
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
setRef = element => {
this.ref = element;
};
handleClickOutside = e => {
if (this.ref && !this.ref.contains(e.target))
this.setState({
open: false
});
};
handleSelect = selectedValue => {
const { name, onChange } = this.props;
this.setState({
open: false,
selectedValue
});
if (onChange) {
onChange(null, {
name,
value: selectedValue
});
}
};
toggle = () => {
this.setState({
open: !this.state.open
});
};
render() {
const { options, placeholder, style } = this.props;
const { open, selectedValue } = this.state;
const activeOption = options.find(opt => opt.value === selectedValue);
return (
<div ref={this.setRef} className={styles.dropdown} style={style}>
<div className={styles.container}>
<p onClick={this.toggle} className={styles.button}>
{activeOption ? activeOption.text : placeholder}
</p>
<ul
className={classNames(styles.menu, {
[styles.isOpen]: open
})}
>
{options.map((option, index) => (
<li
key={option.key}
role="option"
aria-selected={selectedValue === option.value}
className={classNames(styles.item, {
[styles.active]: selectedValue
? selectedValue === option.value
: index === 0
})}
onClick={() => this.handleSelect(option.value)}
>
{option.text}
</li>
))}
</ul>
</div>
</div>
);
}
}
Dropdown.defaultProps = {
placeholder: 'Click to select'
};
/**
*
* @param {Function} callback
*/
export function useClickOutside(callback) {
const ref = useRef(null);
function handleClickOutside(e) {
if (ref && !ref.current.contains(e.target)) callback();
}
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return function dispose() {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
return [ref, handleClickOutside];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment