Skip to content

Commit

Permalink
Merge pull request react-bootstrap#810 from react-bootstrap/modal-imp…
Browse files Browse the repository at this point in the history
…rovements

Modal improvements
  • Loading branch information
mtscout6 committed Jun 15, 2015
2 parents 9352546 + a0034e1 commit 6c7a5f0
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/examples/ModalStatic.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const modalInstance = (
<div className='static-modal'>
<Modal title='Modal title'
enforceFocus={false}
backdrop={false}
animation={false}
container={mountNode}
Expand Down
162 changes: 149 additions & 13 deletions src/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,68 @@ import EventListener from './utils/EventListener';
// - Add `modal-body` div if only one child passed in that doesn't already have it
// - Tests

/**
* Gets the correct clientHeight of the modal container
* when the body/window/document you need to use the docElement clientHeight
* @param {HTMLElement} container
* @param {ReactElement|HTMLElement} context
* @return {Number}
*/
function containerClientHeight(container, context) {
let doc = domUtils.ownerDocument(context);

return (container === doc.body || container === doc.documentElement)
? doc.documentElement.clientHeight
: container.clientHeight;
}

function getContainer(context){
return (context.props.container && React.findDOMNode(context.props.container)) ||
domUtils.ownerDocument(context).body;
}

/**
* Firefox doesn't have a focusin event so using capture is easiest way to get bubbling
* IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8
* @param {ReactElement|HTMLElement} context
* @param {Function} handler
*/
function onFocus(context, handler) {
let doc = domUtils.ownerDocument(context);
let useFocusin = !doc.addEventListener;
let remove;

if (useFocusin) {
document.attachEvent('onfocusin', handler);
remove = () => document.detachEvent('onfocusin', handler);
} else {
document.addEventListener('focus', handler, true);
remove = () => document.removeEventListener('focus', handler, true);
}
return { remove };
}

let scrollbarSize;

if (domUtils.canUseDom) {
let scrollDiv = document.createElement('div');

scrollDiv.style.position = 'absolute';
scrollDiv.style.top = '-9999px';
scrollDiv.style.width = '50px';
scrollDiv.style.height = '50px';
scrollDiv.style.overflow = 'scroll';

document.body.appendChild(scrollDiv);

scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;

document.body.removeChild(scrollDiv);
scrollDiv = null;
}

const Modal = React.createClass({

mixins: [BootstrapMixin, FadeMixin],

propTypes: {
Expand All @@ -21,7 +82,8 @@ const Modal = React.createClass({
closeButton: React.PropTypes.bool,
animation: React.PropTypes.bool,
onRequestHide: React.PropTypes.func.isRequired,
dialogClassName: React.PropTypes.string
dialogClassName: React.PropTypes.string,
enforceFocus: React.PropTypes.bool
},

getDefaultProps() {
Expand All @@ -30,13 +92,20 @@ const Modal = React.createClass({
backdrop: true,
keyboard: true,
animation: true,
closeButton: true
closeButton: true,
enforceFocus: true
};
},

getInitialState(){
return { };
},

render() {
let modalStyle = {display: 'block'};
let state = this.state;
let modalStyle = { ...state.dialogStyles, display: 'block'};
let dialogClasses = this.getBsClassSet();

delete dialogClasses.modal;
dialogClasses['modal-dialog'] = true;

Expand Down Expand Up @@ -66,7 +135,7 @@ const Modal = React.createClass({
);

return this.props.backdrop ?
this.renderBackdrop(modal) : modal;
this.renderBackdrop(modal, state.backdropStyles) : modal;
},

renderBackdrop(modal) {
Expand All @@ -91,8 +160,8 @@ const Modal = React.createClass({
let closeButton;
if (this.props.closeButton) {
closeButton = (
<button type="button" className="close" aria-hidden="true" onClick={this.props.onRequestHide}>&times;</button>
);
<button type="button" className="close" aria-hidden="true" onClick={this.props.onRequestHide}>&times;</button>
);
}

return (
Expand All @@ -119,30 +188,63 @@ const Modal = React.createClass({
},

componentDidMount() {
const doc = domUtils.ownerDocument(this);
const win = domUtils.ownerWindow(this);

this._onDocumentKeyupListener =
EventListener.listen(domUtils.ownerDocument(this), 'keyup', this.handleDocumentKeyUp);
EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);

this._onWindowResizeListener =
EventListener.listen(win, 'resize', this.handleWindowResize);

if (this.props.enforceFocus) {
this._onFocusinListener = onFocus(this, this.enforceFocus);
}

let container = getContainer(this);

let container = (this.props.container && React.findDOMNode(this.props.container)) ||
domUtils.ownerDocument(this).body;
container.className += container.className.length ? ' modal-open' : 'modal-open';

this.focusModalContent();
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);

this._originalPadding = container.style.paddingRight;

if (this._containerIsOverflowing) {
container.style.paddingRight = parseInt(this._originalPadding || 0, 10) + scrollbarSize + 'px';
}

if (this.props.backdrop) {
this.iosClickHack();
}

this.setState(this._getStyles() //eslint-disable-line react/no-did-mount-set-state
, () => this.focusModalContent());
},

componentDidUpdate(prevProps) {
if (this.props.backdrop && this.props.backdrop !== prevProps.backdrop) {
this.iosClickHack();
this.setState(this._getStyles()); //eslint-disable-line react/no-did-update-set-state
}

if (this.props.container !== prevProps.container) {
let container = getContainer(this);
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
}
},

componentWillUnmount() {
this._onDocumentKeyupListener.remove();
let container = (this.props.container && React.findDOMNode(this.props.container)) ||
domUtils.ownerDocument(this).body;
this._onWindowResizeListener.remove();

if (this._onFocusinListener) {
this._onFocusinListener.remove();
}

let container = getContainer(this);

container.style.paddingRight = this._originalPadding;

container.className = container.className.replace(/ ?modal-open/, '');

this.restoreLastFocus();
Expand All @@ -162,8 +264,12 @@ const Modal = React.createClass({
}
},

handleWindowResize() {
this.setState(this._getStyles());
},

focusModalContent () {
this.lastFocus = domUtils.ownerDocument(this).activeElement;
this.lastFocus = domUtils.activeElement(this);
let modalContent = React.findDOMNode(this.refs.modal);
modalContent.focus();
},
Expand All @@ -173,6 +279,36 @@ const Modal = React.createClass({
this.lastFocus.focus();
this.lastFocus = null;
}
},

enforceFocus() {
if ( !this.isMounted() ) {
return;
}

let active = domUtils.activeElement(this);
let modal = React.findDOMNode(this.refs.modal);

if (modal !== active && !domUtils.contains(modal, active)){
modal.focus();
}
},

_getStyles() {
if ( !domUtils.canUseDom ) { return {}; }

let node = React.findDOMNode(this.refs.modal);
let scrollHt = node.scrollHeight;
let container = getContainer(this);
let containerIsOverflowing = this._containerIsOverflowing;
let modalIsOverflowing = scrollHt > containerClientHeight(container, this);

return {
dialogStyles: {
paddingRight: containerIsOverflowing && !modalIsOverflowing ? scrollbarSize : void 0,
paddingLeft: !containerIsOverflowing && modalIsOverflowing ? scrollbarSize : void 0
}
};
}
});

Expand Down
32 changes: 32 additions & 0 deletions src/utils/domUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import React from 'react';


let canUseDom = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);


/**
* Get elements owner document
*
Expand All @@ -11,6 +19,27 @@ function ownerDocument(componentOrElement) {
return (elem && elem.ownerDocument) || document;
}

function ownerWindow(componentOrElement) {
let doc = ownerDocument(componentOrElement);
return doc.defaultView
? doc.defaultView
: doc.parentWindow;
}

/**
* get the active element, safe in IE
* @return {HTMLElement}
*/
function getActiveElement(componentOrElement){
let doc = ownerDocument(componentOrElement);

try {
return doc.activeElement || doc.body;
} catch (e) {
return doc.body;
}
}

/**
* Shortcut to compute element style
*
Expand Down Expand Up @@ -138,10 +167,13 @@ function contains(elem, inner){
}

export default {
canUseDom,
contains,
ownerWindow,
ownerDocument,
getComputedStyles,
getOffset,
getPosition,
activeElement: getActiveElement,
offsetParent: offsetParentFunc
};

0 comments on commit 6c7a5f0

Please sign in to comment.