From 607e01166d48c44fbc49cfb9adf5c3d0b83a0c14 Mon Sep 17 00:00:00 2001 From: Guillaume Hammadi Date: Fri, 10 Jun 2016 15:47:57 +1000 Subject: [PATCH] Push `lib` folder to resemble official npm package --- lib/components/Tab.js | 72 +++++++ lib/components/TabList.js | 31 +++ lib/components/TabPanel.js | 52 +++++ lib/components/Tabs.js | 340 ++++++++++++++++++++++++++++++++ lib/helpers/childrenPropType.js | 56 ++++++ lib/helpers/styles.js | 50 +++++ lib/helpers/uuid.js | 7 + lib/main.js | 43 ++++ 8 files changed, 651 insertions(+) create mode 100644 lib/components/Tab.js create mode 100644 lib/components/TabList.js create mode 100644 lib/components/TabPanel.js create mode 100644 lib/components/Tabs.js create mode 100644 lib/helpers/childrenPropType.js create mode 100644 lib/helpers/styles.js create mode 100644 lib/helpers/uuid.js create mode 100644 lib/main.js diff --git a/lib/components/Tab.js b/lib/components/Tab.js new file mode 100644 index 0000000000..acf8635535 --- /dev/null +++ b/lib/components/Tab.js @@ -0,0 +1,72 @@ +'use strict'; + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _reactDom = require('react-dom'); + +var _classnames = require('classnames'); + +var _classnames2 = _interopRequireDefault(_classnames); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function syncNodeAttributes(node, props) { + if (props.selected) { + node.setAttribute('tabindex', 0); + node.setAttribute('selected', 'selected'); + if (props.focus) { + node.focus(); + } + } else { + node.removeAttribute('tabindex'); + node.removeAttribute('selected'); + } +} + +module.exports = _react2.default.createClass({ + displayName: 'Tab', + + propTypes: { + className: _react.PropTypes.string, + id: _react.PropTypes.string, + selected: _react.PropTypes.bool, + disabled: _react.PropTypes.bool, + panelId: _react.PropTypes.string, + children: _react.PropTypes.oneOfType([_react.PropTypes.array, _react.PropTypes.object, _react.PropTypes.string]) + }, + + getDefaultProps: function getDefaultProps() { + return { + focus: false, + selected: false, + id: null, + panelId: null + }; + }, + componentDidMount: function componentDidMount() { + syncNodeAttributes((0, _reactDom.findDOMNode)(this), this.props); + }, + componentDidUpdate: function componentDidUpdate() { + syncNodeAttributes((0, _reactDom.findDOMNode)(this), this.props); + }, + render: function render() { + return _react2.default.createElement( + 'li', + { + className: (0, _classnames2.default)('ReactTabs__Tab', this.props.className, { + 'ReactTabs__Tab--selected': this.props.selected, + 'ReactTabs__Tab--disabled': this.props.disabled + }), + role: 'tab', + id: this.props.id, + 'aria-selected': this.props.selected ? 'true' : 'false', + 'aria-expanded': this.props.selected ? 'true' : 'false', + 'aria-disabled': this.props.disabled ? 'true' : 'false', + 'aria-controls': this.props.panelId + }, + this.props.children + ); + } +}); \ No newline at end of file diff --git a/lib/components/TabList.js b/lib/components/TabList.js new file mode 100644 index 0000000000..3ebc8b6948 --- /dev/null +++ b/lib/components/TabList.js @@ -0,0 +1,31 @@ +'use strict'; + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _classnames = require('classnames'); + +var _classnames2 = _interopRequireDefault(_classnames); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +module.exports = _react2.default.createClass({ + displayName: 'TabList', + + propTypes: { + className: _react.PropTypes.string, + children: _react.PropTypes.oneOfType([_react.PropTypes.object, _react.PropTypes.array]) + }, + + render: function render() { + return _react2.default.createElement( + 'ul', + { + className: (0, _classnames2.default)('ReactTabs__TabList', this.props.className), + role: 'tablist' + }, + this.props.children + ); + } +}); \ No newline at end of file diff --git a/lib/components/TabPanel.js b/lib/components/TabPanel.js new file mode 100644 index 0000000000..1b899286cf --- /dev/null +++ b/lib/components/TabPanel.js @@ -0,0 +1,52 @@ +'use strict'; + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _classnames = require('classnames'); + +var _classnames2 = _interopRequireDefault(_classnames); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +module.exports = _react2.default.createClass({ + displayName: 'TabPanel', + + propTypes: { + className: _react.PropTypes.string, + selected: _react.PropTypes.bool, + id: _react.PropTypes.string, + tabId: _react.PropTypes.string, + children: _react.PropTypes.oneOfType([_react.PropTypes.array, _react.PropTypes.object, _react.PropTypes.string]) + }, + + contextTypes: { + forceRenderTabPanel: _react.PropTypes.bool + }, + + getDefaultProps: function getDefaultProps() { + return { + selected: false, + id: null, + tabId: null + }; + }, + render: function render() { + var children = this.context.forceRenderTabPanel || this.props.selected ? this.props.children : null; + + return _react2.default.createElement( + 'div', + { + className: (0, _classnames2.default)('ReactTabs__TabPanel', this.props.className, { + 'ReactTabs__TabPanel--selected': this.props.selected + }), + role: 'tabpanel', + id: this.props.id, + 'aria-labelledby': this.props.tabId, + style: { display: this.props.selected ? null : 'none' } + }, + children + ); + } +}); \ No newline at end of file diff --git a/lib/components/Tabs.js b/lib/components/Tabs.js new file mode 100644 index 0000000000..2a623ca18a --- /dev/null +++ b/lib/components/Tabs.js @@ -0,0 +1,340 @@ +'use strict'; + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _reactDom = require('react-dom'); + +var _classnames = require('classnames'); + +var _classnames2 = _interopRequireDefault(_classnames); + +var _jsStylesheet = require('js-stylesheet'); + +var _jsStylesheet2 = _interopRequireDefault(_jsStylesheet); + +var _uuid = require('../helpers/uuid'); + +var _uuid2 = _interopRequireDefault(_uuid); + +var _childrenPropType = require('../helpers/childrenPropType'); + +var _childrenPropType2 = _interopRequireDefault(_childrenPropType); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// Determine if a node from event.target is a Tab element +function isTabNode(node) { + return node.nodeName === 'LI' && node.getAttribute('role') === 'tab'; +} + +// Determine if a tab node is disabled +function isTabDisabled(node) { + return node.getAttribute('aria-disabled') === 'true'; +} + +var useDefaultStyles = true; + +module.exports = _react2.default.createClass({ + displayName: 'Tabs', + + propTypes: { + className: _react.PropTypes.string, + selectedIndex: _react.PropTypes.number, + onSelect: _react.PropTypes.func, + focus: _react.PropTypes.bool, + children: _childrenPropType2.default, + forceRenderTabPanel: _react.PropTypes.bool + }, + + childContextTypes: { + forceRenderTabPanel: _react.PropTypes.bool + }, + + statics: { + setUseDefaultStyles: function setUseDefaultStyles(use) { + useDefaultStyles = use; + } + }, + + getDefaultProps: function getDefaultProps() { + return { + selectedIndex: -1, + focus: false, + forceRenderTabPanel: false + }; + }, + getInitialState: function getInitialState() { + return this.copyPropsToState(this.props); + }, + getChildContext: function getChildContext() { + return { + forceRenderTabPanel: this.props.forceRenderTabPanel + }; + }, + componentDidMount: function componentDidMount() { + if (useDefaultStyles) { + (0, _jsStylesheet2.default)(require('../helpers/styles.js')); // eslint-disable-line global-require + } + }, + componentWillReceiveProps: function componentWillReceiveProps(newProps) { + this.setState(this.copyPropsToState(newProps)); + }, + setSelected: function setSelected(index, focus) { + // Don't do anything if nothing has changed + if (index === this.state.selectedIndex) return; + // Check index boundary + if (index < 0 || index >= this.getTabsCount()) return; + + // Keep reference to last index for event handler + var last = this.state.selectedIndex; + + // Update selected index + this.setState({ selectedIndex: index, focus: focus === true }); + + // Call change event handler + if (typeof this.props.onSelect === 'function') { + this.props.onSelect(index, last); + } + }, + getNextTab: function getNextTab(index) { + var count = this.getTabsCount(); + + // Look for non-disabled tab from index to the last tab on the right + for (var i = index + 1; i < count; i++) { + var tab = this.getTab(i); + if (!isTabDisabled((0, _reactDom.findDOMNode)(tab))) { + return i; + } + } + + // If no tab found, continue searching from first on left to index + for (var _i = 0; _i < index; _i++) { + var _tab = this.getTab(_i); + if (!isTabDisabled((0, _reactDom.findDOMNode)(_tab))) { + return _i; + } + } + + // No tabs are disabled, return index + return index; + }, + getPrevTab: function getPrevTab(index) { + var i = index; + + // Look for non-disabled tab from index to first tab on the left + while (i--) { + var tab = this.getTab(i); + if (!isTabDisabled((0, _reactDom.findDOMNode)(tab))) { + return i; + } + } + + // If no tab found, continue searching from last tab on right to index + i = this.getTabsCount(); + while (i-- > index) { + var _tab2 = this.getTab(i); + if (!isTabDisabled((0, _reactDom.findDOMNode)(_tab2))) { + return i; + } + } + + // No tabs are disabled, return index + return index; + }, + getTabsCount: function getTabsCount() { + return this.props.children && this.props.children[0] ? _react2.default.Children.count(this.props.children[0].props.children) : 0; + }, + getPanelsCount: function getPanelsCount() { + return _react2.default.Children.count(this.props.children.slice(1)); + }, + getTabList: function getTabList() { + return this.refs.tablist; + }, + getTab: function getTab(index) { + return this.refs['tabs-' + index]; + }, + getPanel: function getPanel(index) { + return this.refs['panels-' + index]; + }, + getChildren: function getChildren() { + var index = 0; + var count = 0; + var children = this.props.children; + var state = this.state; + var tabIds = this.tabIds = this.tabIds || []; + var panelIds = this.panelIds = this.panelIds || []; + var diff = this.tabIds.length - this.getTabsCount(); + + // Add ids if new tabs have been added + // Don't bother removing ids, just keep them in case they are added again + // This is more efficient, and keeps the uuid counter under control + while (diff++ < 0) { + tabIds.push((0, _uuid2.default)()); + panelIds.push((0, _uuid2.default)()); + } + + // Map children to dynamically setup refs + return _react2.default.Children.map(children, function (child) { + // null happens when conditionally rendering TabPanel/Tab + // see https://github.com/rackt/react-tabs/issues/37 + if (child === null) { + return null; + } + + var result = null; + + // Clone TabList and Tab components to have refs + if (count++ === 0) { + // TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel` + result = (0, _react.cloneElement)(child, { + ref: 'tablist', + children: _react2.default.Children.map(child.props.children, function (tab) { + // null happens when conditionally rendering TabPanel/Tab + // see https://github.com/rackt/react-tabs/issues/37 + if (tab === null) { + return null; + } + + var ref = 'tabs-' + index; + var id = tabIds[index]; + var panelId = panelIds[index]; + var selected = state.selectedIndex === index; + var focus = selected && state.focus; + + index++; + + return (0, _react.cloneElement)(tab, { + ref: ref, + id: id, + panelId: panelId, + selected: selected, + focus: focus + }); + }) + }); + + // Reset index for panels + index = 0; + } + // Clone TabPanel components to have refs + else { + var ref = 'panels-' + index; + var id = panelIds[index]; + var tabId = tabIds[index]; + var selected = state.selectedIndex === index; + + index++; + + result = (0, _react.cloneElement)(child, { + ref: ref, + id: id, + tabId: tabId, + selected: selected + }); + } + + return result; + }); + }, + handleKeyDown: function handleKeyDown(e) { + if (isTabNode(e.target)) { + var index = this.state.selectedIndex; + var preventDefault = false; + + // Select next tab to the left + if (e.keyCode === 37 || e.keyCode === 38) { + index = this.getPrevTab(index); + preventDefault = true; + } + // Select next tab to the right + /* eslint brace-style:0 */ + else if (e.keyCode === 39 || e.keyCode === 40) { + index = this.getNextTab(index); + preventDefault = true; + } + + // This prevents scrollbars from moving around + if (preventDefault) { + e.preventDefault(); + } + + this.setSelected(index, true); + } + }, + handleClick: function handleClick(e) { + var node = e.target; + do { + // eslint-disable-line no-cond-assign + if (isTabNode(node)) { + if (isTabDisabled(node)) { + return; + } + + var index = [].slice.call(node.parentNode.children).indexOf(node); + this.setSelected(index); + return; + } + } while ((node = node.parentNode) !== null); + }, + + + // This is an anti-pattern, so sue me + copyPropsToState: function copyPropsToState(props) { + var selectedIndex = props.selectedIndex; + + // If no selectedIndex prop was supplied, then try + // preserving the existing selectedIndex from state. + // If the state has not selectedIndex, default + // to the first tab in the TabList. + // + // TODO: Need automation testing around this + // Manual testing can be done using examples/focus + // See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js + if (selectedIndex === -1) { + if (this.state && this.state.selectedIndex) { + selectedIndex = this.state.selectedIndex; + } else { + selectedIndex = 0; + } + } + + return { + selectedIndex: selectedIndex, + focus: props.focus + }; + }, + render: function render() { + var _this = this; + + // This fixes an issue with focus management. + // + // Ultimately, when focus is true, and an input has focus, + // and any change on that input causes a state change/re-render, + // focus gets sent back to the active tab, and input loses focus. + // + // Since the focus state only needs to be remembered + // for the current render, we can reset it once the + // render has happened. + // + // Don't use setState, because we don't want to re-render. + // + // See https://github.com/rackt/react-tabs/pull/7 + if (this.state.focus) { + setTimeout(function () { + _this.state.focus = false; + }, 0); + } + + return _react2.default.createElement( + 'div', + { + className: (0, _classnames2.default)('ReactTabs', 'react-tabs', this.props.className), + onClick: this.handleClick, + onKeyDown: this.handleKeyDown + }, + this.getChildren() + ); + } +}); \ No newline at end of file diff --git a/lib/helpers/childrenPropType.js b/lib/helpers/childrenPropType.js new file mode 100644 index 0000000000..248ec8f99d --- /dev/null +++ b/lib/helpers/childrenPropType.js @@ -0,0 +1,56 @@ +'use strict'; + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _Tab = require('../components/Tab'); + +var _Tab2 = _interopRequireDefault(_Tab); + +var _TabList = require('../components/TabList'); + +var _TabList2 = _interopRequireDefault(_TabList); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +module.exports = function childrenPropTypes(props, propName) { + var error = void 0; + var tabsCount = 0; + var panelsCount = 0; + var children = props[propName]; + + _react2.default.Children.forEach(children, function (child) { + // null happens when conditionally rendering TabPanel/Tab + // see https://github.com/rackt/react-tabs/issues/37 + if (child === null) { + return; + } + + if (child.type === _TabList2.default) { + _react2.default.Children.forEach(child.props.children, function (c) { + // null happens when conditionally rendering TabPanel/Tab + // see https://github.com/rackt/react-tabs/issues/37 + if (c === null) { + return; + } + + if (c.type === _Tab2.default) { + tabsCount++; + } else { + error = new Error('Expected \'Tab\' but found \'' + (c.type.displayName || c.type) + '\''); + } + }); + } else if (child.type.displayName === 'TabPanel') { + panelsCount++; + } else { + error = new Error('Expected \'TabList\' or \'TabPanel\' but found \'' + (child.type.displayName || child.type) + '\''); + } + }); + + if (tabsCount !== panelsCount) { + error = new Error("There should be an equal number of 'Tabs' and 'TabPanels'." + ('Received ' + tabsCount + ' \'Tabs\' and ' + panelsCount + ' \'TabPanels\'.')); + } + + return error; +}; \ No newline at end of file diff --git a/lib/helpers/styles.js b/lib/helpers/styles.js new file mode 100644 index 0000000000..d5a8c14a32 --- /dev/null +++ b/lib/helpers/styles.js @@ -0,0 +1,50 @@ +'use strict'; + +module.exports = { + '.react-tabs [role=tablist]': { + 'border-bottom': '1px solid #aaa', + margin: '0 0 10px', + padding: '0' + }, + + '.react-tabs [role=tab]': { + display: 'inline-block', + border: '1px solid transparent', + 'border-bottom': 'none', + bottom: '-1px', + position: 'relative', + 'list-style': 'none', + padding: '6px 12px', + cursor: 'pointer' + }, + + '.react-tabs [role=tab][aria-selected=true]': { + background: '#fff', + 'border-color': '#aaa', + color: 'black', + 'border-radius': '5px 5px 0 0', + '-moz-border-radius': '5px 5px 0 0', + '-webkit-border-radius': '5px 5px 0 0' + }, + + '.react-tabs [role=tab][aria-disabled=true]': { + color: 'GrayText', + cursor: 'default' + }, + + '.react-tabs [role=tab]:focus': { + 'box-shadow': '0 0 5px hsl(208, 99%, 50%)', + 'border-color': 'hsl(208, 99%, 50%)', + outline: 'none' + }, + + '.react-tabs [role=tab]:focus:after': { + content: '""', + position: 'absolute', + height: '5px', + left: '-4px', + right: '-4px', + bottom: '-5px', + background: '#fff' + } +}; \ No newline at end of file diff --git a/lib/helpers/uuid.js b/lib/helpers/uuid.js new file mode 100644 index 0000000000..fa14a74104 --- /dev/null +++ b/lib/helpers/uuid.js @@ -0,0 +1,7 @@ +"use strict"; + +// Get a universally unique identifier +var count = 0; +module.exports = function uuid() { + return "react-tabs-" + count++; +}; \ No newline at end of file diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000000..5da4b39194 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,43 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _Tabs = require('./components/Tabs'); + +Object.defineProperty(exports, 'Tabs', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_Tabs).default; + } +}); + +var _TabList = require('./components/TabList'); + +Object.defineProperty(exports, 'TabList', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_TabList).default; + } +}); + +var _Tab = require('./components/Tab'); + +Object.defineProperty(exports, 'Tab', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_Tab).default; + } +}); + +var _TabPanel = require('./components/TabPanel'); + +Object.defineProperty(exports, 'TabPanel', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_TabPanel).default; + } +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } \ No newline at end of file