diff --git a/.eslintrc b/.eslintrc
index a1f25b83c6..e889f7dd81 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,6 +12,8 @@
],
"rules": {
"comma-spacing": 2,
+ "comma-style": [2, "last"],
+ "one-var": [2, { "initialized": "never" }],
"key-spacing": 0,
"no-underscore-dangle": 0,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 526cb14ccc..725b336127 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+v0.23.4 - Tue, 16 Jun 2015 00:37:04 GMT
+---------------------------------------
+
+- [0ce46b9](../../commit/0ce46b9) [changed] only autofocus modals when enforceFocus is true (the default)
+- [c5855d2](../../commit/c5855d2) [changed] createChainedFunction to chain many functions, and to throw if non-functions are provided.
+- [d18dadb](../../commit/d18dadb) [fixed] container content no longer shifts when overflowing
+- [66f0f92](../../commit/66f0f92) [added] enforceFocus prop to Modal
+- [3869ca2](../../commit/3869ca2) [fixed] Modal doesn't "jump" when container is overflowing
+
+
+
v0.23.3 - Fri, 12 Jun 2015 21:46:30 GMT
---------------------------------------
diff --git a/docs/build.js b/docs/build.js
index f167e47641..0bf9095c42 100644
--- a/docs/build.js
+++ b/docs/build.js
@@ -23,7 +23,9 @@ const readmeDest = path.join(docsBuilt, 'README.md');
*/
function generateHTML(fileName) {
return new Promise((resolve, reject) => {
- Router.run(routes, '/' + fileName, Handler => {
+ const urlSlug = fileName === 'index.html' ? '/' : `/${fileName}`;
+
+ Router.run(routes, urlSlug, Handler => {
let html = React.renderToString(React.createElement(Handler));
html = '' + html;
let write = fsp.writeFile(path.join(docsBuilt, fileName), html);
diff --git a/docs/dev-run b/docs/dev-run
index 68c047112e..e34cfb48ee 100755
--- a/docs/dev-run
+++ b/docs/dev-run
@@ -1,4 +1,3 @@
-#!/usr/bin/env babel-node
/* eslint no-process-exit: 0 */
import 'colors';
import portfinder from 'portfinder';
diff --git a/docs/examples/ModalStatic.js b/docs/examples/ModalStatic.js
index 545efbee73..f01a4d058e 100644
--- a/docs/examples/ModalStatic.js
+++ b/docs/examples/ModalStatic.js
@@ -1,6 +1,7 @@
const modalInstance = (
+
-
diff --git a/package.json b/package.json
index 86d5c5664e..348c77c3cd 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"test": "npm run lint && npm run build && karma start --single-run && _mocha --compilers js:babel-core/register test/server/*Spec.js",
"lint": "eslint ./",
"docs-build": "babel-node tools/build-cli.js --docs-only",
- "docs": "docs/dev-run",
+ "docs": "babel-node docs/dev-run",
"docs-prod": "npm run docs-build && NODE_ENV=production babel-node docs/server.js",
"docs-prod-unoptimized": "npm run docs-build -- --dev && NODE_ENV=production babel-node docs/server.js"
},
@@ -103,4 +103,4 @@
"babel-runtime": "^5.1.10",
"classnames": "^2.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/src/CollapsibleNav.js b/src/CollapsibleNav.js
index 1063764f60..427d0b30f3 100644
--- a/src/CollapsibleNav.js
+++ b/src/CollapsibleNav.js
@@ -29,9 +29,9 @@ const CollapsibleNav = React.createClass({
for (let key in nodes) {
if (nodes.hasOwnProperty(key)) {
- let n = React.findDOMNode(nodes[key])
- , h = n.offsetHeight
- , computedStyles = domUtils.getComputedStyles(n);
+ let n = React.findDOMNode(nodes[key]);
+ let h = n.offsetHeight;
+ let computedStyles = domUtils.getComputedStyles(n);
height += (h +
parseInt(computedStyles.marginTop, 10) +
diff --git a/src/FadeMixin.js b/src/FadeMixin.js
index ddef53e59e..b5597d013d 100644
--- a/src/FadeMixin.js
+++ b/src/FadeMixin.js
@@ -57,8 +57,8 @@ export default {
},
componentWillUnmount() {
- let els = getElementsAndSelf(React.findDOMNode(this), ['fade']),
- container = (this.props.container && React.findDOMNode(this.props.container)) ||
+ let els = getElementsAndSelf(React.findDOMNode(this), ['fade']);
+ let container = (this.props.container && React.findDOMNode(this.props.container)) ||
domUtils.ownerDocument(this).body;
if (els.length) {
diff --git a/src/Modal.js b/src/Modal.js
index a91cbe962f..bcd6c2db7e 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -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: {
@@ -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() {
@@ -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;
@@ -66,7 +135,7 @@ const Modal = React.createClass({
);
return this.props.backdrop ?
- this.renderBackdrop(modal) : modal;
+ this.renderBackdrop(modal, state.backdropStyles) : modal;
},
renderBackdrop(modal) {
@@ -91,8 +160,8 @@ const Modal = React.createClass({
let closeButton;
if (this.props.closeButton) {
closeButton = (
-
- );
+
+ );
}
return (
@@ -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();
@@ -162,10 +264,17 @@ const Modal = React.createClass({
}
},
+ handleWindowResize() {
+ this.setState(this._getStyles());
+ },
+
focusModalContent () {
- this.lastFocus = domUtils.ownerDocument(this).activeElement;
- let modalContent = React.findDOMNode(this.refs.modal);
- modalContent.focus();
+ if (this.props.enforceFocus) {
+ this.lastFocus = domUtils.activeElement(this);
+
+ let modalContent = React.findDOMNode(this.refs.modal);
+ modalContent.focus();
+ }
},
restoreLastFocus () {
@@ -173,6 +282,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
+ }
+ };
}
});
diff --git a/src/Nav.js b/src/Nav.js
index 46049bf41f..ed3b0d2ac9 100644
--- a/src/Nav.js
+++ b/src/Nav.js
@@ -36,9 +36,9 @@ const Nav = React.createClass({
},
getCollapsibleDimensionValue() {
- let node = React.findDOMNode(this.refs.ul),
- height = node.offsetHeight,
- computedStyles = domUtils.getComputedStyles(node);
+ let node = React.findDOMNode(this.refs.ul);
+ let height = node.offsetHeight;
+ let computedStyles = domUtils.getComputedStyles(node);
return height + parseInt(computedStyles.marginTop, 10) + parseInt(computedStyles.marginBottom, 10);
},
diff --git a/src/utils/createChainedFunction.js b/src/utils/createChainedFunction.js
index 61064ebfde..05a34cc3e2 100644
--- a/src/utils/createChainedFunction.js
+++ b/src/utils/createChainedFunction.js
@@ -4,22 +4,26 @@
* Will only create a new function if needed,
* otherwise will pass back existing functions or null.
*
- * @param {function} one
- * @param {function} two
+ * @param {function} functions to chain
* @returns {function|null}
*/
-function createChainedFunction(one, two) {
- let hasOne = typeof one === 'function';
- let hasTwo = typeof two === 'function';
+function createChainedFunction(...funcs) {
+ return funcs
+ .filter(f => f != null)
+ .reduce((acc, f) => {
+ if (typeof f !== 'function') {
+ throw new Error('Invalid Argument Type, must only provide functions, undefined, or null.');
+ }
- if (!hasOne && !hasTwo) { return null; }
- if (!hasOne) { return two; }
- if (!hasTwo) { return one; }
+ if (acc === null) {
+ return f;
+ }
- return function chainedFunction() {
- one.apply(this, arguments);
- two.apply(this, arguments);
- };
+ return function chainedFunction(...args) {
+ acc.apply(this, args);
+ f.apply(this, args);
+ };
+ }, null);
}
export default createChainedFunction;
diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js
index cbac69af5d..31fa7c0340 100644
--- a/src/utils/domUtils.js
+++ b/src/utils/domUtils.js
@@ -1,5 +1,13 @@
import React from 'react';
+
+let canUseDom = !!(
+ typeof window !== 'undefined' &&
+ window.document &&
+ window.document.createElement
+);
+
+
/**
* Get elements owner document
*
@@ -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
*
@@ -138,10 +167,13 @@ function contains(elem, inner){
}
export default {
+ canUseDom,
contains,
+ ownerWindow,
ownerDocument,
getComputedStyles,
getOffset,
getPosition,
+ activeElement: getActiveElement,
offsetParent: offsetParentFunc
};
diff --git a/test/ModalSpec.js b/test/ModalSpec.js
index e81209d9e1..0e516d52dc 100644
--- a/test/ModalSpec.js
+++ b/test/ModalSpec.js
@@ -156,6 +156,36 @@ describe('Modal', function () {
}, 0);
});
+ it('Should not focus on the Modal when enforceFocus is false', function (done) {
+
+ document.activeElement.should.equal(focusableContainer);
+
+ let Container = React.createClass({
+ getInitialState() {
+ return {modalOpen: true};
+ },
+ render() {
+ if (this.state.modalOpen) {
+ return (
+ {}} container={this}>
+ Message
+
+ );
+ } else {
+ return ;
+ }
+ }
+ });
+
+ React.render(, focusableContainer);
+
+ setTimeout(function () {
+ // modal should be focused when opened
+ document.activeElement.should.equal(focusableContainer);
+ done();
+ }, 0);
+ });
});
+
});
diff --git a/test/PanelSpec.js b/test/PanelSpec.js
index 8380a36431..6247c110b6 100644
--- a/test/PanelSpec.js
+++ b/test/PanelSpec.js
@@ -36,10 +36,10 @@ describe('Panel', function () {
});
it('Should have custom component header', function () {
- let header = Heading
,
- instance = ReactTestUtils.renderIntoDocument(
- Panel content
- );
+ let header = Heading
;
+ let instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
header = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'panel-heading'));
assert.equal(header.firstChild.nodeName, 'H3');
assert.ok(header.firstChild.className.match(/\bpanel-title\b/));
@@ -47,10 +47,10 @@ describe('Panel', function () {
});
it('Should have custom component header with anchor', function () {
- let header = Heading
,
- instance = ReactTestUtils.renderIntoDocument(
- Panel content
- );
+ let header = Heading
;
+ let instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
header = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'panel-heading'));
assert.equal(header.firstChild.nodeName, 'H3');
assert.ok(header.firstChild.className.match(/\bpanel-title\b/));
@@ -59,10 +59,10 @@ describe('Panel', function () {
});
it('Should have custom component header with custom class', function () {
- let header = Heading
,
- instance = ReactTestUtils.renderIntoDocument(
- Panel content
- );
+ let header = Heading
;
+ let instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
header = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'panel-heading'));
assert.equal(header.firstChild.nodeName, 'H3');
assert.ok(header.firstChild.className.match(/\bpanel-title\b/));
diff --git a/test/utils/createChainedFunctionSpec.js b/test/utils/createChainedFunctionSpec.js
new file mode 100644
index 0000000000..4b022d16e8
--- /dev/null
+++ b/test/utils/createChainedFunctionSpec.js
@@ -0,0 +1,71 @@
+/* eslint no-new-func: 0 */
+import createChainedFunction from '../../src/utils/createChainedFunction';
+
+describe('createChainedFunction', function() {
+ it('returns null with no arguments', function() {
+ expect(createChainedFunction()).to.equal(null);
+ });
+
+ it('returns original function when single function is provided', function() {
+ const func1 = sinon.stub();
+ createChainedFunction(func1).should.equal(func1);
+ });
+
+ it('wraps two functions with another that invokes both when called', function() {
+ const func1 = sinon.stub();
+ const func2 = sinon.stub();
+ const chained = createChainedFunction(func1, func2);
+
+ chained
+ .should.not.equal(func1)
+ .and.should.not.equal(func2);
+
+ func1.should.not.have.been.called;
+ func2.should.not.have.been.called;
+
+ chained();
+
+ func1.should.have.been.calledOnce;
+ func2.should.have.been.calledOnce;
+ });
+
+ it('wraps multiple functions and invokes them in the order provided', function() {
+ const results = [];
+ const func1 = () => results.push(1);
+ const func2 = () => results.push(2);
+ const func3 = () => results.push(3);
+ const chained = createChainedFunction(func1, func2, func3);
+ chained();
+ results.should.eql([1, 2, 3]);
+ });
+
+ it('forwards arguments to all chained functions', function() {
+ const in1 = 'herpa derpa';
+ const in2 = {
+ herpa: 'derpa'
+ };
+
+ const func = (arg1, arg2) => {
+ arg1.should.equal(in1);
+ arg2.should.equal(in2);
+ };
+
+ const chained = createChainedFunction(func, func, func);
+ chained(in1, in2);
+ });
+
+ it('throws when func is not provided', function() {
+ expect(() => {
+ createChainedFunction({ herpa: 'derpa' });
+ }).to.throw(/Invalid Argument Type/);
+ });
+
+ it('works with new Function call', function() {
+ const results = [];
+ const func1 = new Function('results', 'results.push(1);');
+ const func2 = new Function('results', 'results.push(2);');
+ const chained = createChainedFunction(func1, func2);
+ chained(results);
+ results.should.eql([1, 2]);
+ });
+});