Skip to content

Commit

Permalink
Merge 778b015 into 16e3814
Browse files Browse the repository at this point in the history
  • Loading branch information
carlobernardini committed Jun 12, 2019
2 parents 16e3814 + 778b015 commit c38cfbf
Show file tree
Hide file tree
Showing 34 changed files with 1,317 additions and 149 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"prettier"
],
"env": {
"browser": true,
"jest": true
},
"parser": "babel-eslint",
Expand Down
498 changes: 350 additions & 148 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@babel/polyfill": "7.0.0",
"css-vars-ponyfill": "1.16.4",
"diacritics": "1.3.0",
"prop-types": "^15.7.2"
},
"peerDependencies": {
Expand Down Expand Up @@ -78,6 +79,7 @@
"eslint-plugin-jsx-a11y": "6.1.1",
"eslint-plugin-prettier": "2.7.0",
"eslint-plugin-react": "7.11.1",
"fetch-mock": "7.3.0",
"html-webpack-plugin": "3.2.0",
"husky": "1.2.0",
"jest": "24.8.0",
Expand Down
197 changes: 197 additions & 0 deletions src/components/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import React, { createRef, PureComponent } from 'react';
import PropTypes from 'prop-types';
import bem from 'bem';
import Button from '../Button';
import DropdownContent from './DropdownContent';
import IconCaret from '../Icon/IconCaret';
import { DropdownProvider } from './DropdownContext';
import styles from './Dropdown.scss';
import { CONTEXTS, SIZES } from '../../constants';

class Dropdown extends PureComponent {
constructor(props) {
super(props);

const { initiallyOpened = false, value = null } = props;

this.state = {
expanded: initiallyOpened,
filterValue: null,
selection: value
};

this.dropdown = createRef();
this.dropdownContent = createRef();
}

componentDidMount = () => {
window.addEventListener('click', this.handleClickOutside);
this.dropdown.current.addEventListener('keyup', this.handleEscPress, true);
};

componentWillUnmount = () => {
window.removeEventListener('click', this.handleClickOutside);
this.dropdown.current.removeEventListener('keyup', this.handleEscPress, true);
};

handleChange = selection => {
const { multiselect, onChange } = this.props;

onChange(selection);

if (!multiselect) {
this.toggleDropdown(null, true);
}
};

handleClickOutside = ({ target }) => {
// Collapse dropdown on click outside
if (!this.dropdownContent || !this.dropdownContent.current) {
return false;
}
if (this.dropdownContent.current.contains(target)) {
return false;
}
const { expanded } = this.state;
if (!expanded) {
return false;
}
this.toggleDropdown(null, true);
return true;
};

handleEscPress = event => {
// Collapse dropdown on esc press
event.stopPropagation();
const key = event.keyCode || event.which;

if (key !== 27) {
return false;
}

this.toggleDropdown(null, true);
return false;
};

handleSetFilter = event => {
const {
target: { value }
} = event;
this.setState({
filterValue: value
});
};

toggleDropdown = (event, collapse = false) => {
const { expanded } = this.state;

if (event && event.stopPropagation) {
event.stopPropagation();
}

const newState = {
expanded: collapse ? false : !expanded
};

if (collapse) {
newState.filterValue = null;
}

this.setState(newState);

return true;
};

render() {
const {
children,
initiallyOpened,
label,
multiselect,
renderButton,
selectedLabel,
...rest
} = this.props;

const { expanded, filterValue, selection } = this.state;
const buttonLabel = selectedLabel ? selectedLabel(selection) : label;
const dropdownButton = renderButton({
props: {
...rest,
onClick: this.toggleDropdown
},
label: buttonLabel,
caret: <IconCaret {...this.elem('caret')} />
});

return (
<div {...this.block()} ref={this.dropdown}>
<DropdownProvider
value={{
filterValue,
handleChange: this.handleChange,
multiselect,
selection,
setFilter: this.handleSetFilter
}}
>
{dropdownButton}
{!!expanded && (
<DropdownContent ref={this.dropdownContent} role="menu" aria-expanded shown>
{children}
</DropdownContent>
)}
</DropdownProvider>
</div>
);
}
}

Dropdown.displayName = 'Dropdown';

Dropdown.propTypes = {
/** The dropdown content */
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
/** Context for this dropdown */
context: PropTypes.oneOf(['link', ...CONTEXTS]),
/** Wether the dropdown should be expanded when rendered */
initiallyOpened: PropTypes.bool,
/** Renders block-level dropdown trigger */
isBlock: PropTypes.bool,
/** Label for the dropdown trigger */
label: PropTypes.node.isRequired,
/** Wether it is possible to select multiple items */
multiselect: PropTypes.bool,
/** Callback function to be triggered when selecting items */
onChange: PropTypes.func,
/** Custom dropdown trigger component */
renderButton: PropTypes.func,
/** Returns the label to be shown in case of a selection */
selectedLabel: PropTypes.func,
/** Size for the dropdown trigger */
size: PropTypes.oneOf(SIZES),
/** Current selection */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array])
};

Dropdown.defaultProps = {
children: null,
context: 'neutral',
initiallyOpened: false,
isBlock: false,
multiselect: false,
onChange: () => {},
renderButton: ({ props, label, caret }) => (
<Button {...props}>
{label}
{caret}
</Button>
),
selectedLabel: null,
size: 'normal',
value: null
};

Dropdown.propsToMods = ['context'];

export default bem(styles)(Dropdown);
22 changes: 22 additions & 0 deletions src/components/Dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.Dropdown {
display: inline-block;
position: relative;
font: {
family: var(--font-family-primary);
size: var(--font-size-normal);
}

&__caret {
margin-left: .5em;
fill: var(--color-background);

&--context {
&_link {
fill: var(--link-color-normal);
}
&_neutral {
fill: var(--color-foreground);
}
}
}
}
34 changes: 34 additions & 0 deletions src/components/Dropdown/DropdownContent/DropdownContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import bem from 'bem';
import styles from './DropdownContent.scss';

const { block } = bem({
name: 'DropdownContent',
classnames: styles,
propsToMods: ['shown']
});

const DropdownContent = forwardRef((props, ref) => {
const { children, shown, ...rest } = props;

return (
<div {...rest} {...block(props)} ref={ref}>
{children}
</div>
);
});

DropdownContent.displayName = 'DropdownContent';

DropdownContent.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
shown: PropTypes.bool
};

DropdownContent.defaultProps = {
children: null,
shown: false
};

export default DropdownContent;
36 changes: 36 additions & 0 deletions src/components/Dropdown/DropdownContent/DropdownContent.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@keyframes listIn {
from {
opacity: .5;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.DropdownContent {
animation: listIn .1s linear;
background-color: var(--color-background);
border: {
color: var(--color-neutral);
radius: var(--border-radius);
style: solid;
width: var(--line-normal);
}
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
display: none;
margin-top: var(--spacing-normal);
min-width: 100%;
overflow: auto;
overflow-x: hidden;
position: absolute;
top: 100%;
left: 0;
z-index: 2;

&--shown {
display: block;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import toJson from 'enzyme-to-json';
import DropdownContent from '../DropdownContent';

describe('<DropdownContent>', () => {
it('should render a dropdown container correctly', () => {
const wrapper = shallow(<DropdownContent>Some content</DropdownContent>);
expect(toJson(wrapper)).toMatchSnapshot();
});

it('should apply classes according to props', () => {
const wrapper = shallow(<DropdownContent shown>Some content</DropdownContent>);
expect(wrapper.props().className).toEqual('DropdownContent DropdownContent--shown');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<DropdownContent> should render a dropdown container correctly 1`] = `
<div
className="DropdownContent"
>
Some content
</div>
`;
1 change: 1 addition & 0 deletions src/components/Dropdown/DropdownContent/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DropdownContent';
9 changes: 9 additions & 0 deletions src/components/Dropdown/DropdownContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

const DropdownContext = React.createContext({});

DropdownContext.Provider.displayName = 'DropdownProvider';
DropdownContext.Consumer.displayName = 'DropdownConsumer';

export const DropdownProvider = DropdownContext.Provider;
export const DropdownConsumer = DropdownContext.Consumer;
25 changes: 25 additions & 0 deletions src/components/Dropdown/DropdownFilter/DropdownFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import bem from 'bem';
import Input from '../../Input';
import { DropdownConsumer } from '../DropdownContext';
import styles from './DropdownFilter.scss';

const { block } = bem({
name: 'DropdownFilter',
classnames: styles,
propsToMods: []
});

const DropdownFilter = props => (
<DropdownConsumer>
{({ filterValue, setFilter }) => (
<div {...block(props)}>
<Input {...props} isBlock onChange={setFilter} value={filterValue || ''} />
</div>
)}
</DropdownConsumer>
);

DropdownFilter.displayName = 'DropdownFilter';

export default DropdownFilter;
4 changes: 4 additions & 0 deletions src/components/Dropdown/DropdownFilter/DropdownFilter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DropdownFilter {
padding: var(--spacing-3x);
background-color: var(--color-neutral-25);
}
1 change: 1 addition & 0 deletions src/components/Dropdown/DropdownFilter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DropdownFilter';

0 comments on commit c38cfbf

Please sign in to comment.