UNPKG

19 kBJavaScriptView Raw
1function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
2function _extends() { _extends = Object.assign ? Object.assign.bind() : 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; }; return _extends.apply(this, arguments); }
3function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
4function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
5function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
6function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
7function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
8function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
9function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
10function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
11function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
12function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
13function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
14function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
15function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
16import React from 'react';
17import PropTypes from 'prop-types';
18import classNames from 'classnames';
19import Portal from './Portal';
20import Fade from './Fade';
21import { TransitionTimeouts, conditionallyUpdateScrollbar, focusableElements, getOriginalBodyPadding, getTarget, keyCodes, mapToCssModules, omit, setScrollbarWidth, targetPropType } from './utils';
22function noop() {}
23var FadePropTypes = PropTypes.shape(Fade.propTypes);
24var propTypes = {
25 autoFocus: PropTypes.bool,
26 backdrop: PropTypes.bool,
27 backdropClassName: PropTypes.string,
28 backdropTransition: FadePropTypes,
29 children: PropTypes.node,
30 className: PropTypes.string,
31 container: targetPropType,
32 cssModule: PropTypes.object,
33 direction: PropTypes.oneOf(['start', 'end', 'bottom', 'top']),
34 fade: PropTypes.bool,
35 innerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.func]),
36 isOpen: PropTypes.bool,
37 keyboard: PropTypes.bool,
38 labelledBy: PropTypes.string,
39 offcanvasTransition: FadePropTypes,
40 onClosed: PropTypes.func,
41 onEnter: PropTypes.func,
42 onExit: PropTypes.func,
43 style: PropTypes.object,
44 onOpened: PropTypes.func,
45 returnFocusAfterClose: PropTypes.bool,
46 role: PropTypes.string,
47 scrollable: PropTypes.bool,
48 toggle: PropTypes.func,
49 trapFocus: PropTypes.bool,
50 unmountOnClose: PropTypes.bool,
51 zIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
52};
53var propsToOmit = Object.keys(propTypes);
54var defaultProps = {
55 isOpen: false,
56 autoFocus: true,
57 direction: 'start',
58 scrollable: false,
59 role: 'dialog',
60 backdrop: true,
61 keyboard: true,
62 zIndex: 1050,
63 fade: true,
64 onOpened: noop,
65 onClosed: noop,
66 offcanvasTransition: {
67 timeout: TransitionTimeouts.Offcanvas
68 },
69 backdropTransition: {
70 mountOnEnter: true,
71 timeout: TransitionTimeouts.Fade // uses standard fade transition
72 },
73
74 unmountOnClose: true,
75 returnFocusAfterClose: true,
76 container: 'body',
77 trapFocus: false
78};
79var Offcanvas = /*#__PURE__*/function (_React$Component) {
80 _inherits(Offcanvas, _React$Component);
81 var _super = _createSuper(Offcanvas);
82 function Offcanvas(props) {
83 var _this;
84 _classCallCheck(this, Offcanvas);
85 _this = _super.call(this, props);
86 _this._element = null;
87 _this._originalBodyPadding = null;
88 _this.getFocusableChildren = _this.getFocusableChildren.bind(_assertThisInitialized(_this));
89 _this.handleBackdropClick = _this.handleBackdropClick.bind(_assertThisInitialized(_this));
90 _this.handleBackdropMouseDown = _this.handleBackdropMouseDown.bind(_assertThisInitialized(_this));
91 _this.handleEscape = _this.handleEscape.bind(_assertThisInitialized(_this));
92 _this.handleTab = _this.handleTab.bind(_assertThisInitialized(_this));
93 _this.onOpened = _this.onOpened.bind(_assertThisInitialized(_this));
94 _this.onClosed = _this.onClosed.bind(_assertThisInitialized(_this));
95 _this.manageFocusAfterClose = _this.manageFocusAfterClose.bind(_assertThisInitialized(_this));
96 _this.clearBackdropAnimationTimeout = _this.clearBackdropAnimationTimeout.bind(_assertThisInitialized(_this));
97 _this.trapFocus = _this.trapFocus.bind(_assertThisInitialized(_this));
98 _this._backdrop = /*#__PURE__*/React.createRef();
99 _this._dialog = /*#__PURE__*/React.createRef();
100 _this.state = {
101 isOpen: false
102 };
103 return _this;
104 }
105 _createClass(Offcanvas, [{
106 key: "componentDidMount",
107 value: function componentDidMount() {
108 var _this$props = this.props,
109 isOpen = _this$props.isOpen,
110 autoFocus = _this$props.autoFocus,
111 onEnter = _this$props.onEnter;
112 if (isOpen) {
113 this.init();
114 this.setState({
115 isOpen: true
116 });
117 if (autoFocus) {
118 this.setFocus();
119 }
120 }
121 if (onEnter) {
122 onEnter();
123 }
124
125 // traps focus inside the Offcanvas, even if the browser address bar is focused
126 document.addEventListener('focus', this.trapFocus, true);
127 this._isMounted = true;
128 }
129 }, {
130 key: "componentDidUpdate",
131 value: function componentDidUpdate(prevProps, prevState) {
132 if (this.props.isOpen && !prevProps.isOpen) {
133 this.init();
134 this.setState({
135 isOpen: true
136 });
137 return;
138 }
139
140 // now Offcanvas Dialog is rendered and we can refer this._element and this._dialog
141 if (this.props.autoFocus && this.state.isOpen && !prevState.isOpen) {
142 this.setFocus();
143 }
144 if (this._element && prevProps.zIndex !== this.props.zIndex) {
145 this._element.style.zIndex = this.props.zIndex;
146 }
147 }
148 }, {
149 key: "componentWillUnmount",
150 value: function componentWillUnmount() {
151 this.clearBackdropAnimationTimeout();
152 if (this.props.onExit) {
153 this.props.onExit();
154 }
155 if (this._element) {
156 this.destroy();
157 if (this.props.isOpen || this.state.isOpen) {
158 this.close();
159 }
160 }
161 document.removeEventListener('focus', this.trapFocus, true);
162 this._isMounted = false;
163 }
164
165 // not mouseUp because scrollbar fires it, shouldn't close when user scrolls
166 }, {
167 key: "handleBackdropClick",
168 value: function handleBackdropClick(e) {
169 if (e.target === this._mouseDownElement) {
170 e.stopPropagation();
171 var backdrop = this._backdrop.current;
172 if (!this.props.isOpen || this.props.backdrop !== true) return;
173 if (backdrop && e.target === backdrop && this.props.toggle) {
174 this.props.toggle(e);
175 }
176 }
177 }
178 }, {
179 key: "handleTab",
180 value: function handleTab(e) {
181 if (e.which !== 9) return;
182 if (this.offcanvasIndex < Offcanvas.openCount - 1) return; // last opened offcanvas
183
184 var focusableChildren = this.getFocusableChildren();
185 var totalFocusable = focusableChildren.length;
186 if (totalFocusable === 0) return;
187 var currentFocus = this.getFocusedChild();
188 var focusedIndex = 0;
189 for (var i = 0; i < totalFocusable; i += 1) {
190 if (focusableChildren[i] === currentFocus) {
191 focusedIndex = i;
192 break;
193 }
194 }
195 if (e.shiftKey && focusedIndex === 0) {
196 e.preventDefault();
197 focusableChildren[totalFocusable - 1].focus();
198 } else if (!e.shiftKey && focusedIndex === totalFocusable - 1) {
199 e.preventDefault();
200 focusableChildren[0].focus();
201 }
202 }
203 }, {
204 key: "handleBackdropMouseDown",
205 value: function handleBackdropMouseDown(e) {
206 this._mouseDownElement = e.target;
207 }
208 }, {
209 key: "handleEscape",
210 value: function handleEscape(e) {
211 if (this.props.isOpen && e.keyCode === keyCodes.esc && this.props.toggle) {
212 if (this.props.keyboard) {
213 e.preventDefault();
214 e.stopPropagation();
215 this.props.toggle(e);
216 }
217 }
218 }
219 }, {
220 key: "onOpened",
221 value: function onOpened(node, isAppearing) {
222 this.props.onOpened();
223 (this.props.offcanvasTransition.onEntered || noop)(node, isAppearing);
224 }
225 }, {
226 key: "onClosed",
227 value: function onClosed(node) {
228 var unmountOnClose = this.props.unmountOnClose;
229 // so all methods get called before it is unmounted
230 this.props.onClosed();
231 (this.props.offcanvasTransition.onExited || noop)(node);
232 if (unmountOnClose) {
233 this.destroy();
234 }
235 this.close();
236 if (this._isMounted) {
237 this.setState({
238 isOpen: false
239 });
240 }
241 }
242 }, {
243 key: "setFocus",
244 value: function setFocus() {
245 if (this._dialog.current && typeof this._dialog.current.focus === 'function') {
246 this._dialog.current.focus();
247 }
248 }
249 }, {
250 key: "getFocusableChildren",
251 value: function getFocusableChildren() {
252 return this._element.querySelectorAll(focusableElements.join(', '));
253 }
254 }, {
255 key: "getFocusedChild",
256 value: function getFocusedChild() {
257 var currentFocus;
258 var focusableChildren = this.getFocusableChildren();
259 try {
260 currentFocus = document.activeElement;
261 } catch (err) {
262 currentFocus = focusableChildren[0];
263 }
264 return currentFocus;
265 }
266 }, {
267 key: "trapFocus",
268 value: function trapFocus(ev) {
269 if (!this.props.trapFocus) {
270 return;
271 }
272 if (!this._element) {
273 // element is not attached
274 return;
275 }
276 if (this._dialog.current === ev.target) {
277 // initial focus when the Offcanvas is opened
278 return;
279 }
280 if (this.offcanvasIndex < Offcanvas.openCount - 1) {
281 // last opened offcanvas
282 return;
283 }
284 var children = this.getFocusableChildren();
285 for (var i = 0; i < children.length; i += 1) {
286 // focus is already inside the Offcanvas
287 if (children[i] === ev.target) return;
288 }
289 if (children.length > 0) {
290 // otherwise focus the first focusable element in the Offcanvas
291 ev.preventDefault();
292 ev.stopPropagation();
293 children[0].focus();
294 }
295 }
296 }, {
297 key: "init",
298 value: function init() {
299 try {
300 this._triggeringElement = document.activeElement;
301 } catch (err) {
302 this._triggeringElement = null;
303 }
304 if (!this._element) {
305 this._element = document.createElement('div');
306 this._element.setAttribute('tabindex', '-1');
307 this._element.style.position = 'relative';
308 this._element.style.zIndex = this.props.zIndex;
309 this._mountContainer = getTarget(this.props.container);
310 this._mountContainer.appendChild(this._element);
311 }
312 this._originalBodyPadding = getOriginalBodyPadding();
313 conditionallyUpdateScrollbar();
314 if (Offcanvas.openCount === 0 && this.props.backdrop && !this.props.scrollable) {
315 document.body.style.overflow = 'hidden';
316 }
317 this.offcanvasIndex = Offcanvas.openCount;
318 Offcanvas.openCount += 1;
319 }
320 }, {
321 key: "destroy",
322 value: function destroy() {
323 if (this._element) {
324 this._mountContainer.removeChild(this._element);
325 this._element = null;
326 }
327 this.manageFocusAfterClose();
328 }
329 }, {
330 key: "manageFocusAfterClose",
331 value: function manageFocusAfterClose() {
332 if (this._triggeringElement) {
333 var returnFocusAfterClose = this.props.returnFocusAfterClose;
334 if (this._triggeringElement.focus && returnFocusAfterClose) this._triggeringElement.focus();
335 this._triggeringElement = null;
336 }
337 }
338 }, {
339 key: "close",
340 value: function close() {
341 this.manageFocusAfterClose();
342 Offcanvas.openCount = Math.max(0, Offcanvas.openCount - 1);
343 document.body.style.overflow = null;
344 setScrollbarWidth(this._originalBodyPadding);
345 }
346 }, {
347 key: "clearBackdropAnimationTimeout",
348 value: function clearBackdropAnimationTimeout() {
349 if (this._backdropAnimationTimeout) {
350 clearTimeout(this._backdropAnimationTimeout);
351 this._backdropAnimationTimeout = undefined;
352 }
353 }
354 }, {
355 key: "render",
356 value: function render() {
357 var _this$props2 = this.props,
358 direction = _this$props2.direction,
359 unmountOnClose = _this$props2.unmountOnClose;
360 if (!!this._element && (this.state.isOpen || !unmountOnClose)) {
361 var isOffcanvasHidden = !!this._element && !this.state.isOpen && !unmountOnClose;
362 this._element.style.display = isOffcanvasHidden ? 'none' : 'block';
363 var _this$props3 = this.props,
364 className = _this$props3.className,
365 backdropClassName = _this$props3.backdropClassName,
366 cssModule = _this$props3.cssModule,
367 isOpen = _this$props3.isOpen,
368 backdrop = _this$props3.backdrop,
369 role = _this$props3.role,
370 labelledBy = _this$props3.labelledBy,
371 style = _this$props3.style;
372 var offcanvasAttributes = {
373 onKeyUp: this.handleEscape,
374 onKeyDown: this.handleTab,
375 'aria-labelledby': labelledBy,
376 role: role,
377 tabIndex: '-1'
378 };
379 var hasTransition = this.props.fade;
380 var offcanvasTransition = _objectSpread(_objectSpread(_objectSpread({}, Fade.defaultProps), this.props.offcanvasTransition), {}, {
381 baseClass: hasTransition ? this.props.offcanvasTransition.baseClass : '',
382 timeout: hasTransition ? this.props.offcanvasTransition.timeout : 0
383 });
384 var backdropTransition = _objectSpread(_objectSpread(_objectSpread({}, Fade.defaultProps), this.props.backdropTransition), {}, {
385 baseClass: hasTransition ? this.props.backdropTransition.baseClass : '',
386 timeout: hasTransition ? this.props.backdropTransition.timeout : 0
387 });
388 var Backdrop = backdrop && (hasTransition ? /*#__PURE__*/React.createElement(Fade, _extends({}, backdropTransition, {
389 "in": isOpen && !!backdrop,
390 innerRef: this._backdrop,
391 cssModule: cssModule,
392 className: mapToCssModules(classNames('offcanvas-backdrop', backdropClassName), cssModule),
393 onClick: this.handleBackdropClick,
394 onMouseDown: this.handleBackdropMouseDown
395 })) : /*#__PURE__*/React.createElement("div", {
396 className: mapToCssModules(classNames('offcanvas-backdrop', 'show', backdropClassName), cssModule),
397 ref: this._backdrop,
398 onClick: this.handleBackdropClick,
399 onMouseDown: this.handleBackdropMouseDown
400 }));
401 var attributes = omit(this.props, propsToOmit);
402 return /*#__PURE__*/React.createElement(Portal, {
403 node: this._element
404 }, /*#__PURE__*/React.createElement(Fade, _extends({}, attributes, offcanvasAttributes, offcanvasTransition, {
405 "in": isOpen,
406 onEntered: this.onOpened,
407 onExited: this.onClosed,
408 cssModule: cssModule,
409 className: mapToCssModules(classNames('offcanvas', className, "offcanvas-".concat(direction)), cssModule),
410 innerRef: this._dialog,
411 style: _objectSpread(_objectSpread({}, style), {}, {
412 visibility: isOpen ? 'visible' : 'hidden'
413 })
414 }), this.props.children), Backdrop);
415 }
416 return null;
417 }
418 }]);
419 return Offcanvas;
420}(React.Component);
421Offcanvas.propTypes = propTypes;
422Offcanvas.defaultProps = defaultProps;
423Offcanvas.openCount = 0;
424export default Offcanvas;
\No newline at end of file