diff options
Diffstat (limited to 'devtools/client/shared/components/tabs')
-rw-r--r-- | devtools/client/shared/components/tabs/TabBar.js | 378 | ||||
-rw-r--r-- | devtools/client/shared/components/tabs/Tabs.css | 127 | ||||
-rw-r--r-- | devtools/client/shared/components/tabs/Tabs.js | 468 | ||||
-rw-r--r-- | devtools/client/shared/components/tabs/moz.build | 10 |
4 files changed, 983 insertions, 0 deletions
diff --git a/devtools/client/shared/components/tabs/TabBar.js b/devtools/client/shared/components/tabs/TabBar.js new file mode 100644 index 0000000000..730e8c7802 --- /dev/null +++ b/devtools/client/shared/components/tabs/TabBar.js @@ -0,0 +1,378 @@ +/* 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/. */ + +/* eslint-env browser */ + +"use strict"; + +const { + Component, + createFactory, + createRef, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const Sidebar = createFactory( + require("resource://devtools/client/shared/components/Sidebar.js") +); + +loader.lazyRequireGetter( + this, + "Menu", + "resource://devtools/client/framework/menu.js" +); +loader.lazyRequireGetter( + this, + "MenuItem", + "resource://devtools/client/framework/menu-item.js" +); + +// Shortcuts +const { div } = dom; + +/** + * Renders Tabbar component. + */ +class Tabbar extends Component { + static get propTypes() { + return { + children: PropTypes.array, + menuDocument: PropTypes.object, + onSelect: PropTypes.func, + showAllTabsMenu: PropTypes.bool, + allTabsMenuButtonTooltip: PropTypes.string, + activeTabId: PropTypes.string, + renderOnlySelected: PropTypes.bool, + sidebarToggleButton: PropTypes.shape({ + // Set to true if collapsed. + collapsed: PropTypes.bool.isRequired, + // Tooltip text used when the button indicates expanded state. + collapsePaneTitle: PropTypes.string.isRequired, + // Tooltip text used when the button indicates collapsed state. + expandPaneTitle: PropTypes.string.isRequired, + // Click callback + onClick: PropTypes.func.isRequired, + // align toggle button to right + alignRight: PropTypes.bool, + // if set to true toggle-button rotate 90 + canVerticalSplit: PropTypes.bool, + }), + }; + } + + static get defaultProps() { + return { + menuDocument: window.parent.document, + showAllTabsMenu: false, + }; + } + + constructor(props, context) { + super(props, context); + const { activeTabId, children = [] } = props; + const tabs = this.createTabs(children); + const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId); + + this.state = { + activeTab: activeTab === -1 ? 0 : activeTab, + tabs, + }; + + // Array of queued tabs to add to the Tabbar. + this.queuedTabs = []; + + this.createTabs = this.createTabs.bind(this); + this.addTab = this.addTab.bind(this); + this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this); + this.queueTab = this.queueTab.bind(this); + this.toggleTab = this.toggleTab.bind(this); + this.removeTab = this.removeTab.bind(this); + this.select = this.select.bind(this); + this.getTabIndex = this.getTabIndex.bind(this); + this.getTabId = this.getTabId.bind(this); + this.getCurrentTabId = this.getCurrentTabId.bind(this); + this.onTabChanged = this.onTabChanged.bind(this); + this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this); + this.renderTab = this.renderTab.bind(this); + this.tabbarRef = createRef(); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { activeTabId, children = [] } = nextProps; + const tabs = this.createTabs(children); + const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId); + + if ( + activeTab !== this.state.activeTab || + children !== this.props.children + ) { + this.setState({ + activeTab: activeTab === -1 ? 0 : activeTab, + tabs, + }); + } + } + + createTabs(children) { + return children + .filter(panel => panel) + .map((panel, index) => + Object.assign({}, children[index], { + id: panel.props.id || index, + panel, + title: panel.props.title, + }) + ); + } + + // Public API + + addTab(id, title, selected = false, panel, url, index = -1) { + const tabs = this.state.tabs.slice(); + + if (index >= 0) { + tabs.splice(index, 0, { id, title, panel, url }); + } else { + tabs.push({ id, title, panel, url }); + } + + const newState = Object.assign({}, this.state, { + tabs, + }); + + if (selected) { + newState.activeTab = index >= 0 ? index : tabs.length - 1; + } + + this.setState(newState, () => { + if (this.props.onSelect && selected) { + this.props.onSelect(id); + } + }); + } + + addAllQueuedTabs() { + if (!this.queuedTabs.length) { + return; + } + + const tabs = this.state.tabs.slice(); + + // Preselect the first sidebar tab if none was explicitly selected. + let activeTab = 0; + let activeId = this.queuedTabs[0].id; + + for (const { id, index, panel, selected, title, url } of this.queuedTabs) { + if (index >= 0) { + tabs.splice(index, 0, { id, title, panel, url }); + } else { + tabs.push({ id, title, panel, url }); + } + + if (selected) { + activeId = id; + activeTab = index >= 0 ? index : tabs.length - 1; + } + } + + const newState = Object.assign({}, this.state, { + activeTab, + tabs, + }); + + this.setState(newState, () => { + if (this.props.onSelect) { + this.props.onSelect(activeId); + } + }); + + this.queuedTabs = []; + } + + /** + * Queues a tab to be added. This is more performant than calling addTab for every + * single tab to be added since we will limit the number of renders happening when + * a new state is set. Once all the tabs to be added have been queued, call + * addAllQueuedTabs() to populate the TabBar with all the queued tabs. + */ + queueTab(id, title, selected = false, panel, url, index = -1) { + this.queuedTabs.push({ + id, + index, + panel, + selected, + title, + url, + }); + } + + toggleTab(tabId, isVisible) { + const index = this.getTabIndex(tabId); + if (index < 0) { + return; + } + + const tabs = this.state.tabs.slice(); + tabs[index] = Object.assign({}, tabs[index], { + isVisible, + }); + + this.setState( + Object.assign({}, this.state, { + tabs, + }) + ); + } + + removeTab(tabId) { + const index = this.getTabIndex(tabId); + if (index < 0) { + return; + } + + const tabs = this.state.tabs.slice(); + tabs.splice(index, 1); + + let activeTab = this.state.activeTab - 1; + activeTab = activeTab === -1 ? 0 : activeTab; + + this.setState( + Object.assign({}, this.state, { + activeTab, + tabs, + }), + () => { + // Select the next active tab and force the select event handler to initialize + // the panel if needed. + if (tabs.length && this.props.onSelect) { + this.props.onSelect(this.getTabId(activeTab)); + } + } + ); + } + + select(tabId) { + const docRef = this.tabbarRef.current.ownerDocument; + + const index = this.getTabIndex(tabId); + if (index < 0) { + return; + } + + const newState = Object.assign({}, this.state, { + activeTab: index, + }); + + const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`); + + if (tabDomElement) { + tabDomElement.scrollIntoView(); + } + + this.setState(newState, () => { + if (this.props.onSelect) { + this.props.onSelect(tabId); + } + }); + } + + // Helpers + + getTabIndex(tabId) { + let tabIndex = -1; + this.state.tabs.forEach((tab, index) => { + if (tab.id === tabId) { + tabIndex = index; + } + }); + return tabIndex; + } + + getTabId(index) { + return this.state.tabs[index].id; + } + + getCurrentTabId() { + return this.state.tabs[this.state.activeTab].id; + } + + // Event Handlers + + onTabChanged(index) { + this.setState( + { + activeTab: index, + }, + () => { + if (this.props.onSelect) { + this.props.onSelect(this.state.tabs[index].id); + } + } + ); + } + + onAllTabsMenuClick(event) { + const menu = new Menu(); + const 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. + menu.popupAtTarget(target); + + return menu; + } + + // Rendering + + renderTab(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() { + const tabs = this.state.tabs.map(tab => this.renderTab(tab)); + + return div( + { + className: "devtools-sidebar-tabs", + ref: this.tabbarRef, + }, + Sidebar( + { + onAllTabsMenuClick: this.onAllTabsMenuClick, + renderOnlySelected: this.props.renderOnlySelected, + showAllTabsMenu: this.props.showAllTabsMenu, + allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip, + sidebarToggleButton: this.props.sidebarToggleButton, + activeTab: 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 0000000000..876088b667 --- /dev/null +++ b/devtools/client/shared/components/tabs/Tabs.css @@ -0,0 +1,127 @@ +/* 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 { + --tab-height: var(--theme-toolbar-height); + height: 100%; + background: var(--theme-sidebar-background); + display: flex; + flex-direction: column; +} + +.tabs.tabs-tall { + --tab-height: var(--theme-toolbar-tall-height); +} + +/* Hides the tab strip in the TabBar */ +div[hidetabs=true] .tabs .tabs-navigation { + display: none; +} + +.tabs .tabs-navigation { + box-sizing: border-box; + display: flex; + /* Reserve 1px for the border */ + height: calc(var(--tab-height) + 1px); + position: relative; + border-bottom: 1px solid var(--theme-splitter-color); + background: var(--theme-tab-toolbar-background); +} + +.tabs .tabs-menu { + list-style: none; + padding: 0; + margin: 0; + margin-inline-end: 15px; + flex-grow: 1; +} + +/* 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; + overflow-x: scroll; + scrollbar-width: none; +} + +.tabs .tabs-menu-item { + display: inline-block; + position: relative; + margin: 0; + padding: 0; + color: var(--theme-toolbar-color); +} + +.tabs .tabs-menu-item.is-active { + color: var(--theme-toolbar-selected-color); +} + +.tabs .tabs-menu-item:hover { + background-color: var(--theme-toolbar-hover); +} + +.tabs .tabs-menu-item:hover:active:not(.is-active) { + background-color: var(--theme-toolbar-hover-active); +} + +.tabs .tabs-menu-item a { + --text-height: 16px; + --devtools-tab-border-width: 1px; + display: flex; + justify-content: center; + /* Vertically center text, calculate space remaining by taking the full height and removing + the block borders and text. Divide by 2 to distribute above and below. */ + padding: calc((var(--tab-height) - var(--text-height) - (var(--devtools-tab-border-width) * 2)) / 2) 10px; + border: var(--devtools-tab-border-width) solid transparent; + font-size: 12px; + line-height: var(--text-height); + text-decoration: none; + white-space: nowrap; + cursor: default; + user-select: none; + text-align: center; +} + +/* Remove the outline focusring from tabs-menu-item. */ +.tabs .tabs-navigation .tabs-menu-item > a:-moz-focusring { + outline: none; +} + +.tabs .tabs-menu-item .tab-badge { + color: var(--theme-highlight-blue); + font-size: 80%; + font-style: italic; + /* Tabs have a 15px padding start/end, so we set the margins here in order to center the + badge after the tab title, with a 5px effective margin. */ + margin-inline-start: 5px; + margin-inline-end: -10px; +} + +.tabs .tabs-menu-item.is-active .tab-badge { + /* Use the same color as the tab-item when active */ + color: inherit; +} + +/* To avoid "select all" commands from selecting content in hidden tabs */ +.tabs .hidden, +.tabs .hidden * { + user-select: none !important; +} + +/* Make sure panel content takes entire vertical space. */ +.tabs .panels { + flex: 1; + overflow: hidden; +} + +.tabs .tab-panel { + height: 100%; + overflow-x: hidden; + overflow-y: auto; +} diff --git a/devtools/client/shared/components/tabs/Tabs.js b/devtools/client/shared/components/tabs/Tabs.js new file mode 100644 index 0000000000..a265032f9e --- /dev/null +++ b/devtools/client/shared/components/tabs/Tabs.js @@ -0,0 +1,468 @@ +/* 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 { + Component, + createRef, + } = require("devtools/client/shared/vendor/react"); + const dom = require("devtools/client/shared/vendor/react-dom-factories"); + const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); + + /** + * 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> + */ + class Tabs extends Component { + static get propTypes() { + return { + className: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.string, + PropTypes.object, + ]), + activeTab: PropTypes.number, + onMount: PropTypes.func, + onBeforeChange: PropTypes.func, + onAfterChange: PropTypes.func, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]) + .isRequired, + showAllTabsMenu: PropTypes.bool, + allTabsMenuButtonTooltip: PropTypes.string, + onAllTabsMenuClick: PropTypes.func, + tall: PropTypes.bool, + + // To render a sidebar toggle button before the tab menu provide a function that + // returns a React component for the button. + renderSidebarToggle: PropTypes.func, + // Set true will only render selected panel on DOM. It's complete + // opposite of the created array, and it's useful if panels content + // is unpredictable and update frequently. + renderOnlySelected: PropTypes.bool, + }; + } + + static get defaultProps() { + return { + activeTab: 0, + showAllTabsMenu: false, + renderOnlySelected: false, + }; + } + + constructor(props) { + super(props); + + this.state = { + activeTab: props.activeTab, + + // This array is used to store an object containing information on whether a tab + // at a specified index has already been created (e.g. selected at least once) and + // the tab id. An example of the object structure is the following: + // [{ isCreated: true, tabId: "ruleview" }, { isCreated: false, tabId: "foo" }]. + // If the tab at the specified index has already been created, 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, + }; + + this.tabsEl = createRef(); + + this.onOverflow = this.onOverflow.bind(this); + this.onUnderflow = this.onUnderflow.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onClickTab = this.onClickTab.bind(this); + this.setActive = this.setActive.bind(this); + this.renderMenuItems = this.renderMenuItems.bind(this); + this.renderPanels = this.renderPanels.bind(this); + } + + componentDidMount() { + const node = this.tabsEl.current; + node.addEventListener("keydown", this.onKeyDown); + + // 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); + node.addEventListener("underflow", this.onUnderflow); + } + + const index = this.state.activeTab; + if (this.props.onMount) { + this.props.onMount(index); + } + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + let { children, activeTab } = nextProps; + const panels = children.filter(panel => panel); + let created = [...this.state.created]; + + // If the children props has changed due to an addition or removal of a tab, + // update the state's created array with the latest tab ids and whether or not + // the tab is already created. + if (this.state.created.length != panels.length) { + created = panels.map(panel => { + // Get whether or not the tab has already been created from the previous state. + const createdEntry = this.state.created.find(entry => { + return entry && entry.tabId === panel.props.id; + }); + const isCreated = !!createdEntry && createdEntry.isCreated; + const tabId = panel.props.id; + + return { + isCreated, + tabId, + }; + }); + } + + // Check type of 'activeTab' props to see if it's valid (it's 0-based index). + if (typeof activeTab === "number") { + // Reset to index 0 if index overflows the range of panel array + activeTab = activeTab < panels.length && activeTab >= 0 ? activeTab : 0; + + created[activeTab] = Object.assign({}, created[activeTab], { + isCreated: true, + }); + + this.setState({ + activeTab, + }); + } + + this.setState({ + created, + }); + } + + componentWillUnmount() { + const node = this.tabsEl.current; + node.removeEventListener("keydown", this.onKeyDown); + + if (this.props.showAllTabsMenu) { + node.removeEventListener("overflow", this.onOverflow); + node.removeEventListener("underflow", this.onUnderflow); + } + } + + // DOM Events + + onOverflow(event) { + if (event.target.classList.contains("tabs-menu")) { + this.setState({ + overflow: true, + }); + } + } + + onUnderflow(event) { + if (event.target.classList.contains("tabs-menu")) { + this.setState({ + overflow: false, + }); + } + } + + onKeyDown(event) { + // Bail out if the focus isn't on a tab. + if (!event.target.closest(".tabs-menu-item")) { + return; + } + + let activeTab = this.state.activeTab; + const tabCount = this.props.children.length; + + const ltr = event.target.ownerDocument.dir == "ltr"; + const nextOrLastTab = Math.min(tabCount - 1, activeTab + 1); + const previousOrFirstTab = Math.max(0, activeTab - 1); + + switch (event.code) { + case "ArrowRight": + if (ltr) { + activeTab = nextOrLastTab; + } else { + activeTab = previousOrFirstTab; + } + break; + case "ArrowLeft": + if (ltr) { + activeTab = previousOrFirstTab; + } else { + activeTab = nextOrLastTab; + } + break; + } + + if (this.state.activeTab != activeTab) { + this.setActive(activeTab); + } + } + + onClickTab(index, event) { + this.setActive(index); + + if (event) { + event.preventDefault(); + } + } + + onMouseDown(event) { + // Prevents click-dragging the tab headers + if (event) { + event.preventDefault(); + } + } + + // API + + setActive(index) { + const onAfterChange = this.props.onAfterChange; + const onBeforeChange = this.props.onBeforeChange; + + if (onBeforeChange) { + const cancel = onBeforeChange(index); + if (cancel) { + return; + } + } + + const created = [...this.state.created]; + created[index] = Object.assign({}, created[index], { + isCreated: true, + }); + + const newState = Object.assign({}, this.state, { + created, + activeTab: index, + }); + + this.setState(newState, () => { + // Properly set focus on selected tab. + const selectedTab = this.tabsEl.current.querySelector(".is-active > a"); + if (selectedTab) { + selectedTab.focus(); + } + + if (onAfterChange) { + onAfterChange(index); + } + }); + } + + // Rendering + + renderMenuItems() { + 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]; + } + + const tabs = this.props.children + .map(tab => (typeof tab === "function" ? tab() : tab)) + .filter(tab => tab) + .map((tab, index) => { + const { + id, + className: tabClassName, + title, + badge, + showBadge, + } = tab.props; + + const ref = "tab-menu-" + index; + const isTabSelected = this.state.activeTab === index; + + const className = [ + "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( + { + className, + key: index, + ref, + role: "presentation", + }, + dom.span({ className: "devtools-tab-line" }), + dom.a( + { + id: id ? id + "-tab" : "tab-" + index, + tabIndex: isTabSelected ? 0 : -1, + title, + "aria-controls": id ? id + "-panel" : "panel-" + index, + "aria-selected": isTabSelected, + role: "tab", + onClick: this.onClickTab.bind(this, index), + onMouseDown: this.onMouseDown.bind(this), + "data-tab-index": index, + }, + title, + badge && !isTabSelected && showBadge() + ? dom.span({ className: "tab-badge" }, badge) + : null + ) + ); + }); + + // Display the menu only if there is not enough horizontal + // space for all tabs (and overflow happened). + const allTabsMenu = this.state.overflow + ? dom.button({ + className: "all-tabs-menu", + title: this.props.allTabsMenuButtonTooltip, + onClick: this.props.onAllTabsMenuClick, + }) + : null; + + // Get the sidebar toggle button if a renderSidebarToggle function is provided. + const sidebarToggle = this.props.renderSidebarToggle + ? this.props.renderSidebarToggle() + : null; + + return dom.nav( + { className: "tabs-navigation" }, + sidebarToggle, + dom.ul({ className: "tabs-menu", role: "tablist" }, tabs), + allTabsMenu + ); + } + + renderPanels() { + let { children, renderOnlySelected } = this.props; + + if (!children) { + throw new Error("There must be at least one Tab"); + } + + if (!Array.isArray(children)) { + children = [children]; + } + + const selectedIndex = this.state.activeTab; + + const panels = children + .map(tab => (typeof tab === "function" ? tab() : tab)) + .filter(tab => tab) + .map((tab, index) => { + const selected = selectedIndex === index; + if (renderOnlySelected && !selected) { + return null; + } + + const id = tab.props.id; + const isCreated = + this.state.created[index] && this.state.created[index].isCreated; + + // Use 'visibility:hidden' + 'height:0' for hiding content of non-selected + // tab. It's faster than 'display:none' because it avoids triggering frame + // destruction and reconstruction. 'width' is not changed to avoid relayout. + const style = { + visibility: selected ? "visible" : "hidden", + height: selected ? "100%" : "0", + }; + + // Allows lazy loading panels by creating them only if they are selected, + // then store a copy of the lazy created panel in `tab.panel`. + if (typeof tab.panel == "function" && selected) { + tab.panel = tab.panel(tab); + } + const panel = tab.panel || tab; + + return dom.div( + { + id: id ? id + "-panel" : "panel-" + index, + key: id, + style, + className: selected ? "tab-panel-box" : "tab-panel-box hidden", + role: "tabpanel", + "aria-labelledby": id ? id + "-tab" : "tab-" + index, + }, + selected || isCreated ? panel : null + ); + }); + + return dom.div({ className: "panels" }, panels); + } + + render() { + return dom.div( + { + className: [ + "tabs", + ...(this.props.tall ? ["tabs-tall"] : []), + this.props.className, + ].join(" "), + ref: this.tabsEl, + }, + this.renderMenuItems(), + this.renderPanels() + ); + } + } + + /** + * Renders simple tab 'panel'. + */ + class Panel extends Component { + static get propTypes() { + return { + id: PropTypes.string.isRequired, + className: PropTypes.string, + title: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]) + .isRequired, + }; + } + + render() { + const { className } = this.props; + return dom.div( + { className: `tab-panel ${className || ""}` }, + this.props.children + ); + } + } + + // Exports from this module + exports.TabPanel = Panel; + exports.Tabs = Tabs; +}); diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build new file mode 100644 index 0000000000..15ede75b9d --- /dev/null +++ b/devtools/client/shared/components/tabs/moz.build @@ -0,0 +1,10 @@ +# -*- 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.js", + "Tabs.js", +) |