Skip to content

Instantly share code, notes, and snippets.

@hibiarata
Last active December 18, 2017 15:20
Show Gist options
  • Save hibiarata/a10d5e48f254e9cb5d8e611ea665dcdd to your computer and use it in GitHub Desktop.
Save hibiarata/a10d5e48f254e9cb5d8e611ea665dcdd to your computer and use it in GitHub Desktop.
React Dropdown Menu
<header>
<h1>React Dropdown Menu Example</h1>
</header>
<div id="main"></div>
<footer>
<a href="https://github.com/mlaursen/react-dd-menu" target="_blank">
<span class="fa fa-github fa-4x"></span>
</a>
</footer>
<script type="text/jsx">
var CSSTransitionGroup = React.addons.CSSTransitionGroup;
var Main = React.createClass({
getInitialState: function() {
return { open: _initialState };
},
close: function(id) {
var open = this.state.open;
open[id] = false;
this.setState({ open: open });
},
toggle: function(id) {
var open = this.state.open;
open[id] = !open[id];
this.setState({ open: open });
},
getToggle: function(text, onClick, isOpen) {
return (
<div className={'tab' + (isOpen ? ' active' : '')}>
<button type="button" onClick={onClick}>{text}</button>
</div>
);
},
click: function(id) {
alert('You have clicked something!');
this.close(id);
},
render: function() {
return (
<main>
{_examples.map(function(example, i) {
return (
<DropdownMenu isOpen={this.state.open[i]} forceCloseFunction={this.close.bind(this, i)}
toggle={this.getToggle(example.text, this.toggle.bind(this, i), this.state.open[i])} direction={example.direction} key={'example' + i} className={example.className}>
<ul>
<DropdownMenuItem component="a" action={this.click.bind(null, i)} childrenProps={{href: "#"}}>Example 1</DropdownMenuItem>
<DropdownMenuItem action={this.click.bind(this, i)}>Example 2</DropdownMenuItem>
<DropdownMenuItem action={this.click.bind(this, i)}>Lorem ipsum pretend</DropdownMenuItem>
<li className="separator" role="separator" />
<DropdownMenuItem action={this.click.bind(this, i)}>Example 3</DropdownMenuItem>
</ul>
</DropdownMenu>
);
}.bind(this))}
</main>
);
}
});
var DropdownMenu = React.createClass({
propTypes: {
isOpen: React.PropTypes.bool.isRequired,
forceCloseFunction: React.PropTypes.func.isRequired,
toggle: React.PropTypes.node.isRequired,
direction: React.PropTypes.oneOf(['center', 'right', 'left']),
className: React.PropTypes.string,
component: React.PropTypes.oneOf(['div', 'span', 'li'])
},
getDefaultProps: function() {
return {
direction: 'center',
className: '',
component: 'div'
};
},
/* Only have the click events enabled when the menu is open */
componentDidUpdate: function(prevProps, prevState) {
if(this.props.isOpen && !prevProps.isOpen) {
window.addEventListener('click', this.handleClickOutside);
} else if(!this.props.isOpen && prevProps.isOpen) {
window.removeEventListener('click', this.handleClickOutside);
}
},
/* If clicked element is not in the dropdown menu children, close menu */
handleClickOutside: function(e) {
var children = this.getDOMNode().getElementsByTagName('*');
for(var x in children) {
if(children[x] == e.target) { return; }
}
this.props.forceCloseFunction(e);
},
handleKeyDown: function(e) {
var key = e.which || e.keyCode;
if(key !== 9) { // tab
return;
}
var items = this.getDOMNode().querySelectorAll('button,a');
var id = e.shiftKey ? 1 : items.length - 1;
if(e.target == items[id]) {
this.props.forceCloseFunction(e);
}
},
render: function() {
var items = this.props.isOpen ? this.props.children : null;
return (
<div className={'dd-menu' + (this.props.className ? ' ' + this.props.className : '')}>
{this.props.toggle}
<CSSTransitionGroup transitionName={'grow-from-' + this.props.direction} component="div"
className="dd-menu-items" onKeyDown={this.handleKeyDown}>
{items}
</CSSTransitionGroup>
</div>
);
}
});
var DropdownMenuItem = React.createClass({
propTypes: {
action: React.PropTypes.func.isRequired,
childrenProps: React.PropTypes.object,
tabIndex: React.PropTypes.number,
component: React.PropTypes.oneOf(['button', 'a']),
className: React.PropTypes.string
},
getDefaultProps: function() {
return {
tabIndex: 0,
component: 'button',
className: '',
childrenProps: {}
};
},
handleKeyDown: function(e) {
var key = e.which || e.keyCode;
if(key === 32) { // spacebar
e.preventDefault(); // prevent page scrolling
this.props.action();
}
},
render: function() {
var children = React.createElement(this.props.component, this.props.childrenProps, this.props.children);
return (
<li className={this.props.className} onClick={this.props.action}>
{children}
</li>
);
}
});
React.render(<Main />, document.getElementById('main'));
</script>
var _examples = [{
direction: 'left',
text: 'Menu From Left'
}, {
direction: 'center',
text: 'Menu From Center'
}, {
direction: 'right',
text: 'Menu From Right'
}, {
direction: 'left',
text: 'Menu From Left/Aligned',
className: 'dd-menu-left'
}, {
direction: 'center',
text: 'Menu From Center/Aligned',
className: 'dd-menu-center'
}, {
direction: 'right',
text: 'Menu From Right/Aligned',
className: 'dd-menu-right'
}, {
direction: 'left',
text: 'Inverse Left/Medium',
className: 'dd-menu-md dd-menu-inverse'
}, {
direction: 'center',
text: 'Inverse Center/Large',
className: 'dd-menu-lg dd-menu-center dd-menu-inverse'
}, {
direction: 'right',
text: 'Inverse Right',
className: 'dd-menu-right dd-menu-inverse'
}];
var _initialState = _examples.map(function() { return false; });
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
@import url(https://fonts.googleapis.com/css?family=Roboto);
$dull-black: #4A4A4A !default;
$black-base: #000000 !default;
$white-base: #ffffff !default;
$peter-river: #3498db;
$turquise: #1abc9c;
$green-sea: #16a085;
$wet-asphalt: #34495e;
$concrete: #95a5a6;
$clouds: #ecf0f1;
$silver: #bdc3c7;
$midnight-blue: #2c3e50;
$roboto: 'Roboto', sans-serif;
/** Animation stuffs */
$default-animation-time: 0.2s !default;
$default-transition-time: 0.2s !default;
$default-shake-distance: 8px !default;
$default-short-slide-distance: 40px !default;
$default-cubic-in: cubic-bezier(0.5, 1.8, 0.9, 0.8);
/* From compass */
@function compact($var-1, $var-2: false,
$var-3: false, $var-4: false,
$var-5: false, $var-6: false,
$var-7: false, $var-8: false,
$var-9: false, $var-10: false) {
$full: $var-1;
$vars: $var-2, $var-3, $var-4, $var-5,
$var-6, $var-7, $var-8, $var-9, $var-10;
@each $var in $vars {
@if $var {
$full: $full, $var;
}
}
@return $full;
}
@function set-arglist-default($arglist, $default) {
$default-index: index($arglist, default);
@if $default-index {
$arglist: set-nth($arglist, $default-index, $default)
}
@return if(length($arglist) > 0, $arglist, $default);
}
/* End copy/paste from compass */
$default-box-shadow-h-offset: 0 !default;
$default-box-shadow-v-offset: 0 !default;
$default-box-shadow-blur: 4px !default;
$default-box-shadow-spread: null !default;
$default-box-shadow-color: rgba(0, 0, 0, .15) !default;
$default-box-shadow-inset: null !default;
/** Box shadows */
@function default-box-shadow() {
@return compact(if($default-box-shadow-inset, inset, null) $default-box-shadow-h-offset $default-box-shadow-v-offset $default-box-shadow-blur $default-box-shadow-color)
}
@mixin box-shadow($shadow...) {
$shadow: set-arglist-default($shadow, default-box-shadow());
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin menu-box-shadow() {
$shadow: default-box-shadow(), 0 2px 4px rgba(0, 0, 0, .29);
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
box-shadow: $shadow;
}
/* ========================================================= */
/* Dropdown menu Source */
/* ========================================================= */
$dd-menu-sm-width: 150px !default;
$dd-menu-md-width: 300px !default;
$dd-menu-lg-width: 450px !default;
$dd-menu-z-index: 7;
$dd-font: $roboto;
$dd-text-color: $black-base;
$dd-list-color: $white-base;
$dd-hover-color: $silver;
$dd-hover-text-color: $black-base;
$dd-inverse-text-color: $white-base;
$dd-inverse-list-color: $midnight-blue;
$dd-inverse-hover-color: $wet-asphalt;
$dd-inverse-hover-text-color: $white-base;
.dd-menu {
display: inline-block;
position: relative;
text-align: center;
&.dd-menu-right {
text-align: right;
.dd-menu-items {
right: 0;
button, a {
text-align: right;
}
}
}
&.dd-menu-left {
text-align: left;
.dd-menu-items {
left: 0;
button, a {
text-align: left;
}
}
}
&.dd-menu-center {
.dd-menu-items {
left: 50%;
transform: translateX(-50%);
}
}
&.dd-menu-sm {
.dd-menu-items {
width: $dd-menu-sm-width;
}
}
&.dd-menu-md {
.dd-menu-items {
width: $dd-menu-md-width;
}
}
&.dd-menu-lg {
.dd-menu-items {
width: $dd-menu-lg-width;
}
}
&.dd-menu-inverse {
> .dd-menu-items {
> ol,
> ul {
color: $dd-inverse-text-color;
background-color: $dd-inverse-list-color;
> li {
&:hover,
> button:focus,
> a:focus {
color: $dd-inverse-hover-text-color;
background-color: $dd-inverse-hover-color;
}
}
}
}
li.separator {
background-color: $wet-asphalt;
}
}
.dd-menu-items {
position: absolute;
z-index: $dd-menu-z-index;
margin-top: 0.5em;
> ul,
> ol {
list-style: none;
padding: 0;
margin: 0;
background-color: $dd-list-color;
color: $dd-text-color;
@include menu-box-shadow();
> li {
&:hover,
> button:focus,
> a:focus {
color: $dd-hover-text-color;
background-color: $dd-hover-color;
outline: none;
}
> a {
text-decoration: none;
color: inherit;
}
> button,
> a {
font-size: 1em;
white-space: pre;
padding: 1em 1.5em;
font-family: $dd-font;
width: 100%;
display: block;
}
> button {
@extend .clear-btn;
}
}
}
}
li.separator {
content: '';
height: 1px;
background-color: rgba(0, 0, 0, .15);
margin-top: .5em;
margin-bottom: .5em;
}
}
/* ========================================================= */
/* Dropdown menu Animations */
/* ========================================================= */
.grow-from-left-enter {
transform: scale(0);
transform-origin: 0 0;
transition: transform $default-transition-time $default-cubic-in;
&.grow-from-left-enter-active {
transform: scale(1);
}
}
.grow-from-left-leave {
transform: scale(1);
transform-origin: 0 0;
transition: transform $default-transition-time ease-out;
&.grow-from-left-leave-active {
transform: scale(0);
}
}
.grow-from-right-enter {
transform: scale(0);
transform-origin: 100% 0;
transition: transform $default-transition-time $default-cubic-in;
&.grow-from-right-enter-active {
transform: scale(1);
}
}
.grow-from-right-leave {
transform: scale(1);
transform-origin: 100% 0;
transition: transform $default-transition-time ease-out;
&.grow-from-right-leave-active {
transform: scale(0);
}
}
.grow-from-center-enter {
transform: scale(0);
transform-origin: 50% 0;
&.grow-from-center-enter-active {
transition: transform $default-transition-time $default-cubic-in;
transform: scale(1);
}
}
.grow-from-center-leave {
transform: scale(1);
transform-origin: 50% 0;
&.grow-from-center-leave-active {
transition: transform $default-transition-time $default-cubic-in;
transform: scale(0);
}
}
/* ========================================================= */
/* Pen Stylings */
/* ========================================================= */
* {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
}
header {
color: $white-base;
font-family: 'Roboto', sans-serif;
background-color: $peter-river;
padding: 1.5em;
display: flex;
justify-content: center;
flex-shrink: 0;
}
#main {
flex-grow: 1;
}
@media (min-width: 768px) {
main {
width: 720px;
}
}
@media (min-width: 992px) {
main {
width: 940px;
}
}
@media (min-width: 1200px) {
main {
width: 1140px;
}
}
main {
margin-left: auto;
margin-right: auto;
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
> .dd-menu {
flex-basis: 330px;
margin: 2em 1.5em;
}
}
footer {
flex-shrink: 0;
display: flex;
justify-content: space-around;
padding: 2em;
> a {
color: $dull-black;
}
}
.clear-btn {
border: none;
background-color: transparent;
}
h1 {
margin: 0;
}
.tab {
> button {
position: relative;
width: 100%;
font-family: 'Roboto', sans-serif;
font-size: 1.2em;
color: $white-base;
padding: 1em 1.5em;
background-color: $turquise;
border: 0;
border-bottom: 2px solid $green-sea;
box-shadow: inset 0 -2px $green-sea;
&:focus {
box-shadow: inset 0 -2px $green-sea, inset 0 0 3px $wet-asphalt;
}
}
&:active,
&.active {
> button {
top: 1px;
outline: none;
box-shadow: none;
}
}
}
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment