react-burger-menu
Version:
An off-canvas sidebar component with a collection of effects and styles using CSS transitions and SVG path animations
500 lines (429 loc) • 17.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', {
value: true
});
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _helpersBaseStyles = require('./helpers/baseStyles');
var _helpersBaseStyles2 = _interopRequireDefault(_helpersBaseStyles);
var _helpersDom = require('./helpers/dom');
var _componentsBurgerIcon = require('./components/BurgerIcon');
var _componentsBurgerIcon2 = _interopRequireDefault(_componentsBurgerIcon);
var _componentsCrossIcon = require('./components/CrossIcon');
var _componentsCrossIcon2 = _interopRequireDefault(_componentsCrossIcon);
exports['default'] = function (styles) {
if (!styles) {
throw new Error('No styles supplied');
}
var ARROW_DOWN = 'ArrowDown';
var ARROW_UP = 'ArrowUp';
var ESCAPE = 'Escape';
var SPACE = ' ';
var HOME = 'Home';
var END = 'End';
function usePrevious(value) {
var ref = _react2['default'].useRef(value);
_react2['default'].useEffect(function () {
ref.current = value;
});
return ref.current;
}
var Menu = function Menu(props) {
var defaultProps = {
bodyClassName: '',
burgerBarClassName: '',
burgerButtonClassName: '',
className: '',
crossButtonClassName: '',
crossClassName: '',
disableAutoFocus: false,
disableCloseOnEsc: false,
htmlClassName: '',
id: '',
itemClassName: '',
itemListClassName: '',
menuClassName: '',
morphShapeClassName: '',
noOverlay: false,
noTransition: false,
onStateChange: function onStateChange() {},
outerContainerId: '',
overlayClassName: '',
pageWrapId: '',
styles: {},
width: 300,
onIconHoverChange: function onIconHoverChange() {},
itemListElement: 'nav'
};
props = _extends({}, defaultProps, props);
var _React$useState = _react2['default'].useState(false);
var _React$useState2 = _slicedToArray(_React$useState, 2);
var isOpen = _React$useState2[0];
var setIsOpen = _React$useState2[1];
var timeoutId = _react2['default'].useRef();
var toggleOptions = _react2['default'].useRef({});
var prevIsOpenProp = usePrevious(props.isOpen);
_react2['default'].useEffect(function () {
if (props.isOpen) {
toggleMenu({ isOpen: true, noStateChange: true });
}
return function cleanup() {
applyWrapperStyles(false);
clearCurrentTimeout();
};
}, []);
_react2['default'].useEffect(function () {
var wasToggled = typeof props.isOpen !== 'undefined' && props.isOpen !== isOpen && props.isOpen !== prevIsOpenProp;
if (wasToggled) {
toggleMenu();
// Toggling changes SVG animation requirements, so defer these until next update
return;
}
if (styles.svg) {
(function () {
var morphShape = document.getElementById('bm-morph-shape');
var path = styles.svg.lib(morphShape).select('path');
if (isOpen) {
// Animate SVG path
styles.svg.animate(path);
} else {
// Reset path (timeout ensures animation happens off screen)
setTimeout(function () {
path.attr('d', styles.svg.pathInitial);
}, 300);
}
})();
}
});
_react2['default'].useEffect(function () {
var _toggleOptions$current = toggleOptions.current;
var noStateChange = _toggleOptions$current.noStateChange;
var focusOnLastItem = _toggleOptions$current.focusOnLastItem;
if (!noStateChange) {
props.onStateChange({ isOpen: isOpen });
}
if (!props.disableAutoFocus) {
// For accessibility reasons, ensures that when we toggle open,
// we focus the first or last menu item according to given parameter
if (isOpen) {
focusOnLastItem ? (0, _helpersDom.focusOnLastMenuItem)() : (0, _helpersDom.focusOnFirstMenuItem)();
} else {
if (document.activeElement) {
document.activeElement.blur();
} else {
document.body.blur(); // Needed for IE
}
}
}
// Timeout ensures wrappers are cleared after animation finishes
clearCurrentTimeout();
timeoutId.current = setTimeout(function () {
timeoutId.current = null;
if (!isOpen) {
applyWrapperStyles(false);
}
}, 500);
// Bind keydown handlers (or custom function if supplied)
var defaultOnKeyDown = isOpen ? onKeyDownOpen : onKeyDownClosed;
var onKeyDown = props.customOnKeyDown || defaultOnKeyDown;
window.addEventListener('keydown', onKeyDown);
return function cleanup() {
window.removeEventListener('keydown', onKeyDown);
};
}, [isOpen]);
function toggleMenu() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
toggleOptions.current = options;
applyWrapperStyles();
// Ensures wrapper styles are applied before the menu is toggled
setTimeout(function () {
setIsOpen(function (open) {
return typeof options.isOpen !== 'undefined' ? options.isOpen : !open;
});
});
}
function open() {
if (typeof props.onOpen === 'function') {
props.onOpen();
} else {
toggleMenu();
}
}
function close() {
if (typeof props.onClose === 'function') {
props.onClose();
} else {
toggleMenu();
}
}
function getStyle(style, index) {
var _props = props;
var width = _props.width;
var right = _props.right;
var formattedWidth = typeof width !== 'string' ? width + 'px' : width;
return style(isOpen, formattedWidth, right, index);
}
// Builds styles incrementally for a given element
function getStyles(el, index, inline) {
var propName = 'bm' + el.replace(el.charAt(0), el.charAt(0).toUpperCase());
// Set base styles
var output = _helpersBaseStyles2['default'][el] ? getStyle(_helpersBaseStyles2['default'][el]) : {};
// Add animation-specific styles
if (styles[el]) {
output = _extends({}, output, getStyle(styles[el], index + 1));
}
// Add custom styles
if (props.styles[propName]) {
output = _extends({}, output, props.styles[propName]);
}
// Add element inline styles
if (inline) {
output = _extends({}, output, inline);
}
// Remove transition if required
// (useful if rendering open initially)
if (props.noTransition) {
delete output.transition;
}
return output;
}
// Sets or unsets styles on DOM elements outside the menu component
// This is necessary for correct page interaction with some of the menus
// Throws and returns if the required external elements don't exist,
// which means any external page animations won't be applied
function handleExternalWrapper(id, wrapperStyles, set) {
var wrapper = document.getElementById(id);
if (!wrapper) {
console.error("Element with ID '" + id + "' not found");
return;
}
var builtStyles = getStyle(wrapperStyles);
for (var prop in builtStyles) {
if (builtStyles.hasOwnProperty(prop)) {
wrapper.style[prop] = set ? builtStyles[prop] : '';
}
}
// Prevent any horizontal scroll
// Only set overflow-x as an inline style if htmlClassName or
// bodyClassName is not passed in. Otherwise, it is up to the caller to
// decide if they want to set the overflow style in CSS using the custom
// class names
var applyOverflow = function applyOverflow(el) {
return el.style['overflow-x'] = set ? 'hidden' : '';
};
if (!props.htmlClassName) {
applyOverflow(document.querySelector('html'));
}
if (!props.bodyClassName) {
applyOverflow(document.querySelector('body'));
}
}
// Applies component-specific styles to external wrapper elements
function applyWrapperStyles() {
var set = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0];
var applyClass = function applyClass(el, className) {
return el.classList[set ? 'add' : 'remove'](className);
};
if (props.htmlClassName) {
applyClass(document.querySelector('html'), props.htmlClassName);
}
if (props.bodyClassName) {
applyClass(document.querySelector('body'), props.bodyClassName);
}
if (styles.pageWrap && props.pageWrapId) {
handleExternalWrapper(props.pageWrapId, styles.pageWrap, set);
}
if (styles.outerContainer && props.outerContainerId) {
handleExternalWrapper(props.outerContainerId, styles.outerContainer, set);
}
var menuWrap = document.querySelector('.bm-menu-wrap');
if (menuWrap) {
if (set) {
menuWrap.removeAttribute('hidden');
} else {
menuWrap.setAttribute('hidden', true);
}
}
}
// Avoids potentially attempting to update an unmounted component
function clearCurrentTimeout() {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
}
function onKeyDownOpen(e) {
e = e || window.event;
switch (e.key) {
case ESCAPE:
// Close on ESC, unless disabled
if (!props.disableCloseOnEsc) {
close();
(0, _helpersDom.focusOnMenuButton)();
}
break;
case ARROW_DOWN:
(0, _helpersDom.focusOnNextMenuItem)();
break;
case ARROW_UP:
(0, _helpersDom.focusOnPreviousMenuItem)();
break;
case HOME:
(0, _helpersDom.focusOnFirstMenuItem)();
break;
case END:
(0, _helpersDom.focusOnLastMenuItem)();
break;
}
}
function onKeyDownClosed(e) {
e = e || window.event;
// Key downs came from menu button
if (e.target === document.getElementById('react-burger-menu-btn')) {
switch (e.key) {
case ARROW_DOWN:
case SPACE:
// If down arrow, space or enter, open menu and focus on first menuitem
toggleMenu();
break;
case ARROW_UP:
// If arrow up, open menu and focus on last menuitem
toggleMenu({ focusOnLastItem: true });
break;
}
}
}
function handleOverlayClick() {
if (props.disableOverlayClick === true || typeof props.disableOverlayClick === 'function' && props.disableOverlayClick()) {
return;
} else {
close();
}
}
return _react2['default'].createElement(
'div',
null,
!props.noOverlay && _react2['default'].createElement('div', {
className: ('bm-overlay ' + props.overlayClassName).trim(),
onClick: handleOverlayClick,
style: getStyles('overlay')
}),
props.customBurgerIcon !== false && _react2['default'].createElement(
'div',
{ style: getStyles('burgerIcon') },
_react2['default'].createElement(_componentsBurgerIcon2['default'], {
onClick: open,
styles: props.styles,
customIcon: props.customBurgerIcon,
className: props.burgerButtonClassName,
barClassName: props.burgerBarClassName,
onIconStateChange: props.onIconStateChange
})
),
_react2['default'].createElement(
'div',
{
id: props.id,
className: ('bm-menu-wrap ' + props.className).trim(),
style: getStyles('menuWrap'),
'aria-hidden': !isOpen
},
styles.svg && _react2['default'].createElement(
'div',
{
id: 'bm-morph-shape',
className: ('bm-morph-shape ' + props.morphShapeClassName).trim(),
style: getStyles('morphShape')
},
_react2['default'].createElement(
'svg',
{
width: '100%',
height: '100%',
viewBox: '0 0 100 800',
preserveAspectRatio: 'none'
},
_react2['default'].createElement('path', { d: styles.svg.pathInitial })
)
),
_react2['default'].createElement(
'div',
{
className: ('bm-menu ' + props.menuClassName).trim(),
style: getStyles('menu')
},
_react2['default'].createElement(props.itemListElement, {
className: ('bm-item-list ' + props.itemListClassName).trim(),
style: getStyles('itemList')
}, _react2['default'].Children.map(props.children, function (item, index) {
if (item) {
var classList = ['bm-item', props.itemClassName, item.props.className].filter(function (className) {
return !!className;
}).join(' ');
var extraProps = _extends({
key: index,
className: classList,
style: getStyles('item', index, item.props.style)
}, !isOpen && { tabIndex: -1 });
return _react2['default'].cloneElement(item, extraProps);
}
}))
),
props.customCrossIcon !== false && _react2['default'].createElement(
'div',
{ style: getStyles('closeButton') },
_react2['default'].createElement(_componentsCrossIcon2['default'], {
onClick: close,
styles: props.styles,
customIcon: props.customCrossIcon,
className: props.crossButtonClassName,
crossClassName: props.crossClassName,
isOpen: isOpen
})
)
)
);
};
Menu.propTypes = {
bodyClassName: _propTypes2['default'].string,
burgerBarClassName: _propTypes2['default'].string,
burgerButtonClassName: _propTypes2['default'].string,
className: _propTypes2['default'].string,
crossButtonClassName: _propTypes2['default'].string,
crossClassName: _propTypes2['default'].string,
customBurgerIcon: _propTypes2['default'].oneOfType([_propTypes2['default'].element, _propTypes2['default'].oneOf([false])]),
customCrossIcon: _propTypes2['default'].oneOfType([_propTypes2['default'].element, _propTypes2['default'].oneOf([false])]),
customOnKeyDown: _propTypes2['default'].func,
disableAutoFocus: _propTypes2['default'].bool,
disableCloseOnEsc: _propTypes2['default'].bool,
disableOverlayClick: _propTypes2['default'].oneOfType([_propTypes2['default'].bool, _propTypes2['default'].func]),
htmlClassName: _propTypes2['default'].string,
id: _propTypes2['default'].string,
isOpen: _propTypes2['default'].bool,
itemClassName: _propTypes2['default'].string,
itemListClassName: _propTypes2['default'].string,
itemListElement: _propTypes2['default'].oneOf(['div', 'nav']),
menuClassName: _propTypes2['default'].string,
morphShapeClassName: _propTypes2['default'].string,
noOverlay: _propTypes2['default'].bool,
noTransition: _propTypes2['default'].bool,
onClose: _propTypes2['default'].func,
onIconHoverChange: _propTypes2['default'].func,
onOpen: _propTypes2['default'].func,
onStateChange: _propTypes2['default'].func,
outerContainerId: styles && styles.outerContainer ? _propTypes2['default'].string.isRequired : _propTypes2['default'].string,
overlayClassName: _propTypes2['default'].string,
pageWrapId: styles && styles.pageWrap ? _propTypes2['default'].string.isRequired : _propTypes2['default'].string,
right: _propTypes2['default'].bool,
styles: _propTypes2['default'].object,
width: _propTypes2['default'].oneOfType([_propTypes2['default'].number, _propTypes2['default'].string])
};
return Menu;
};
module.exports = exports['default'];