summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/tabs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/tabs')
-rw-r--r--devtools/client/shared/components/tabs/moz.build12
-rw-r--r--devtools/client/shared/components/tabs/tabbar.css53
-rw-r--r--devtools/client/shared/components/tabs/tabbar.js204
-rw-r--r--devtools/client/shared/components/tabs/tabs.css183
-rw-r--r--devtools/client/shared/components/tabs/tabs.js369
5 files changed, 821 insertions, 0 deletions
diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build
new file mode 100644
index 000000000..d4d5dc35d
--- /dev/null
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'tabbar.css',
+ 'tabbar.js',
+ 'tabs.css',
+ 'tabs.js',
+)
diff --git a/devtools/client/shared/components/tabs/tabbar.css b/devtools/client/shared/components/tabs/tabbar.css
new file mode 100644
index 000000000..72445e43e
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.tabs .tabs-navigation {
+ line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+ height: 24px;
+}
+
+.tabs .tabs-menu-item:first-child {
+ border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+ outline: var(--theme-focus-outline);
+ outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+ height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+ height: 24px;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+ should stretch accordingly. Use flexbox for the behavior.
+ Use also `overflow: hidden` so, 'overflow' and 'underflow'
+ events are fired (it's utilized by the all-tabs-menu). */
+.tabs .tabs-navigation .tabs-menu {
+ overflow: hidden;
+ display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+ text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 0;
+}
+
diff --git a/devtools/client/shared/components/tabs/tabbar.js b/devtools/client/shared/components/tabs/tabbar.js
new file mode 100644
index 000000000..1e3aa4617
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+ displayName: "Tabbar",
+
+ propTypes: {
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ toolbox: PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabs: [],
+ activeTab: 0
+ };
+ },
+
+ // Public API
+
+ addTab: function (id, title, selected = false, panel, url) {
+ let tabs = this.state.tabs.slice();
+ tabs.push({id, title, panel, url});
+
+ let newState = Object.assign({}, this.state, {
+ tabs: tabs,
+ });
+
+ if (selected) {
+ newState.activeTab = tabs.length - 1;
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect && selected) {
+ this.props.onSelect(id);
+ }
+ });
+ },
+
+ toggleTab: function (tabId, isVisible) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs[index] = Object.assign({}, tabs[index], {
+ isVisible: isVisible
+ });
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ removeTab: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs.splice(index, 1);
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ select: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let newState = Object.assign({}, this.state, {
+ activeTab: index,
+ });
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(tabId);
+ }
+ });
+ },
+
+ // Helpers
+
+ getTabIndex: function (tabId) {
+ let tabIndex = -1;
+ this.state.tabs.forEach((tab, index) => {
+ if (tab.id == tabId) {
+ tabIndex = index;
+ }
+ });
+ return tabIndex;
+ },
+
+ getTabId: function (index) {
+ return this.state.tabs[index].id;
+ },
+
+ getCurrentTabId: function () {
+ return this.state.tabs[this.state.activeTab].id;
+ },
+
+ // Event Handlers
+
+ onTabChanged: function (index) {
+ this.setState({
+ activeTab: index
+ });
+
+ if (this.props.onSelect) {
+ this.props.onSelect(this.state.tabs[index].id);
+ }
+ },
+
+ onAllTabsMenuClick: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of tabs.
+ this.state.tabs.forEach(tab => {
+ menu.append(new MenuItem({
+ label: tab.title,
+ type: "checkbox",
+ checked: this.getCurrentTabId() == tab.id,
+ click: () => this.select(tab.id),
+ }));
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
+
+ return menu;
+ },
+
+ // Rendering
+
+ renderTab: function (tab) {
+ if (typeof tab.panel === "function") {
+ return tab.panel({
+ key: tab.id,
+ title: tab.title,
+ id: tab.id,
+ url: tab.url,
+ });
+ }
+
+ return tab.panel;
+ },
+
+ render: function () {
+ let tabs = this.state.tabs.map(tab => {
+ return this.renderTab(tab);
+ });
+
+ return (
+ div({className: "devtools-sidebar-tabs"},
+ Tabs({
+ onAllTabsMenuClick: this.onAllTabsMenuClick,
+ showAllTabsMenu: this.props.showAllTabsMenu,
+ tabActive: this.state.activeTab,
+ onAfterChange: this.onTabChanged},
+ tabs
+ )
+ )
+ );
+ },
+});
+
+module.exports = Tabbar;
diff --git a/devtools/client/shared/components/tabs/tabs.css b/devtools/client/shared/components/tabs/tabs.css
new file mode 100644
index 000000000..0e70549c5
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -0,0 +1,183 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Tabs General Styles */
+
+.tabs {
+ height: 100%;
+}
+
+.tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.tabs .tabs-menu-item {
+ display: inline-block;
+}
+
+.tabs .tabs-menu-item a {
+ display: block;
+ color: #A9A9A9;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.tabs .tabs-menu-item a {
+ cursor: default;
+}
+
+/* Make sure panel content takes entire vertical space.
+ (minus the height of the tab bar) */
+.tabs .panels {
+ height: calc(100% - 24px);
+}
+
+.tabs .tab-panel {
+ height: 100%;
+}
+
+.tabs .all-tabs-menu {
+ position: absolute;
+ top: 0;
+ offset-inline-end: 0;
+ width: 15px;
+ height: 100%;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+ background: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+/* Light Theme */
+
+.theme-dark .tabs,
+.theme-light .tabs {
+ background: var(--theme-body-background);
+}
+
+.theme-dark .tabs .tabs-navigation,
+.theme-light .tabs .tabs-navigation {
+ position: relative;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .tabs .tabs-menu-item,
+.theme-light .tabs .tabs-menu-item {
+ margin: 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .tabs .tabs-menu-item:last-child,
+.theme-light:not(.theme-firebug) .tabs .tabs-menu-item:last-child {
+ border-inline-end-width: 1px;
+}
+
+.theme-dark .tabs .tabs-menu-item a,
+.theme-light .tabs .tabs-menu-item a {
+ color: var(--theme-content-color1);
+ padding: 3px 15px;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:not(.is-active) {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:active:not(.is-active) {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active,
+.theme-light .tabs .tabs-menu-item.is-active {
+ background-color: var(--theme-selection-background);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active a,
+.theme-light .tabs .tabs-menu-item.is-active a {
+ color: var(--theme-selection-color);
+}
+
+/* Dark Theme */
+
+.theme-dark .tabs .tabs-menu-item a {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active) a {
+ color: #CED3D9;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active a {
+ color: var(--theme-selection-color);
+}
+
+/* Firebug Theme */
+
+.theme-firebug .tabs .tabs-navigation {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+ padding-top: 3px;
+ padding-left: 3px;
+ border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu {
+ margin-bottom: -1px;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+ position: relative;
+ border-inline-start-width: 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:not(.is-active) a {
+ border: 1px solid #C8C8C8;
+ border-bottom: 1px solid transparent;
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active a {
+ background-color: rgb(247, 251, 254);
+ border: 1px solid rgb(170, 188, 207);
+ border-bottom-color: transparent;
+ color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:active a {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active:hover:active a {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ border: 1px solid transparent;
+ padding: 4px 8px;
+}
diff --git a/devtools/client/shared/components/tabs/tabs.js b/devtools/client/shared/components/tabs/tabs.js
new file mode 100644
index 000000000..eaa0738b3
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -0,0 +1,369 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { DOM } = React;
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+
+ /**
+ * Renders simple 'tab' widget.
+ *
+ * Based on ReactSimpleTabs component
+ * https://github.com/pedronauck/react-simpletabs
+ *
+ * Component markup (+CSS) example:
+ *
+ * <div class='tabs'>
+ * <nav class='tabs-navigation'>
+ * <ul class='tabs-menu'>
+ * <li class='tabs-menu-item is-active'>Tab #1</li>
+ * <li class='tabs-menu-item'>Tab #2</li>
+ * </ul>
+ * </nav>
+ * <div class='panels'>
+ * The content of active panel here
+ * </div>
+ * <div>
+ */
+ let Tabs = React.createClass({
+ displayName: "Tabs",
+
+ propTypes: {
+ className: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.string,
+ React.PropTypes.object
+ ]),
+ tabActive: React.PropTypes.number,
+ onMount: React.PropTypes.func,
+ onBeforeChange: React.PropTypes.func,
+ onAfterChange: React.PropTypes.func,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired,
+ showAllTabsMenu: React.PropTypes.bool,
+ onAllTabsMenuClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ tabActive: 0,
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabActive: this.props.tabActive,
+
+ // This array is used to store an information whether a tab
+ // at specific index has already been created (e.g. selected
+ // at least once).
+ // If yes, it's rendered even if not currently selected.
+ // This is because in some cases we don't want to re-create
+ // tab content when it's being unselected/selected.
+ // E.g. in case of an iframe being used as a tab-content
+ // we want the iframe to stay in the DOM.
+ created: [],
+
+ // True if tabs can't fit into available horizontal space.
+ overflow: false,
+ };
+ },
+
+ componentDidMount: function () {
+ let node = findDOMNode(this);
+ node.addEventListener("keydown", this.onKeyDown, false);
+
+ // Register overflow listeners to manage visibility
+ // of all-tabs-menu. This menu is displayed when there
+ // is not enough h-space to render all tabs.
+ // It allows the user to select a tab even if it's hidden.
+ if (this.props.showAllTabsMenu) {
+ node.addEventListener("overflow", this.onOverflow, false);
+ node.addEventListener("underflow", this.onUnderflow, false);
+ }
+
+ let index = this.state.tabActive;
+ if (this.props.onMount) {
+ this.props.onMount(index);
+ }
+ },
+
+ componentWillReceiveProps: function (newProps) {
+ // Check type of 'tabActive' props to see if it's valid
+ // (it's 0-based index).
+ if (typeof newProps.tabActive == "number") {
+ let created = [...this.state.created];
+ created[newProps.tabActive] = true;
+
+ this.setState(Object.assign({}, this.state, {
+ tabActive: newProps.tabActive,
+ created: created,
+ }));
+ }
+ },
+
+ componentWillUnmount: function () {
+ let node = findDOMNode(this);
+ node.removeEventListener("keydown", this.onKeyDown, false);
+
+ if (this.props.showAllTabsMenu) {
+ node.removeEventListener("overflow", this.onOverflow, false);
+ node.removeEventListener("underflow", this.onUnderflow, false);
+ }
+ },
+
+ // DOM Events
+
+ onOverflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: true
+ });
+ }
+ },
+
+ onUnderflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: false
+ });
+ }
+ },
+
+ onKeyDown: function (event) {
+ // Bail out if the focus isn't on a tab.
+ if (!event.target.closest(".tabs-menu-item")) {
+ return;
+ }
+
+ let tabActive = this.state.tabActive;
+ let tabCount = this.props.children.length;
+
+ switch (event.code) {
+ case "ArrowRight":
+ tabActive = Math.min(tabCount - 1, tabActive + 1);
+ break;
+ case "ArrowLeft":
+ tabActive = Math.max(0, tabActive - 1);
+ break;
+ }
+
+ if (this.state.tabActive != tabActive) {
+ this.setActive(tabActive);
+ }
+ },
+
+ onClickTab: function (index, event) {
+ this.setActive(index);
+ event.preventDefault();
+ },
+
+ onAllTabsMenuClick: function (event) {
+ if (this.props.onAllTabsMenuClick) {
+ this.props.onAllTabsMenuClick(event);
+ }
+ },
+
+ // API
+
+ setActive: function (index) {
+ let onAfterChange = this.props.onAfterChange;
+ let onBeforeChange = this.props.onBeforeChange;
+
+ if (onBeforeChange) {
+ let cancel = onBeforeChange(index);
+ if (cancel) {
+ return;
+ }
+ }
+
+ let created = [...this.state.created];
+ created[index] = true;
+
+ let newState = Object.assign({}, this.state, {
+ tabActive: index,
+ created: created
+ });
+
+ this.setState(newState, () => {
+ // Properly set focus on selected tab.
+ let node = findDOMNode(this);
+ let selectedTab = node.querySelector(".is-active > a");
+ if (selectedTab) {
+ selectedTab.focus();
+ }
+
+ if (onAfterChange) {
+ onAfterChange(index);
+ }
+ });
+ },
+
+ // Rendering
+
+ renderMenuItems: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let tabs = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let ref = ("tab-menu-" + index);
+ let title = tab.props.title;
+ let tabClassName = tab.props.className;
+ let isTabSelected = this.state.tabActive === index;
+
+ let classes = [
+ "tabs-menu-item",
+ tabClassName,
+ isTabSelected ? "is-active" : ""
+ ].join(" ");
+
+ // Set tabindex to -1 (except the selected tab) so, it's focusable,
+ // but not reachable via sequential tab-key navigation.
+ // Changing selected tab (and so, moving focus) is done through
+ // left and right arrow keys.
+ // See also `onKeyDown()` event handler.
+ return (
+ DOM.li({
+ ref: ref,
+ key: index,
+ id: "tab-" + index,
+ className: classes,
+ role: "presentation",
+ },
+ DOM.a({
+ tabIndex: this.state.tabActive === index ? 0 : -1,
+ "aria-controls": "panel-" + index,
+ "aria-selected": isTabSelected,
+ role: "tab",
+ onClick: this.onClickTab.bind(this, index),
+ },
+ title
+ )
+ )
+ );
+ });
+
+ // Display the menu only if there is not enough horizontal
+ // space for all tabs (and overflow happened).
+ let allTabsMenu = this.state.overflow ? (
+ DOM.div({
+ className: "all-tabs-menu",
+ onClick: this.props.onAllTabsMenuClick
+ })
+ ) : null;
+
+ return (
+ DOM.nav({className: "tabs-navigation"},
+ DOM.ul({className: "tabs-menu", role: "tablist"},
+ tabs
+ ),
+ allTabsMenu
+ )
+ );
+ },
+
+ renderPanels: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let selectedIndex = this.state.tabActive;
+
+ let panels = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let selected = selectedIndex == index;
+
+ // Use 'visibility:hidden' + 'width/height:0' for hiding
+ // content of non-selected tab. It's faster (not sure why)
+ // than display:none and visibility:collapse.
+ let style = {
+ visibility: selected ? "visible" : "hidden",
+ height: selected ? "100%" : "0",
+ width: selected ? "100%" : "0",
+ };
+
+ return (
+ DOM.div({
+ key: index,
+ id: "panel-" + index,
+ style: style,
+ className: "tab-panel-box",
+ role: "tabpanel",
+ "aria-labelledby": "tab-" + index,
+ },
+ (selected || this.state.created[index]) ? tab : null
+ )
+ );
+ });
+
+ return (
+ DOM.div({className: "panels"},
+ panels
+ )
+ );
+ },
+
+ render: function () {
+ let classNames = ["tabs", this.props.className].join(" ");
+
+ return (
+ DOM.div({className: classNames},
+ this.renderMenuItems(),
+ this.renderPanels()
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders simple tab 'panel'.
+ */
+ let Panel = React.createClass({
+ displayName: "Panel",
+
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired
+ },
+
+ render: function () {
+ return DOM.div({className: "tab-panel"},
+ this.props.children
+ );
+ }
+ });
+
+ // Exports from this module
+ exports.TabPanel = Panel;
+ exports.Tabs = Tabs;
+});