summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/shared
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/shared')
-rw-r--r--devtools/client/debugger/src/components/shared/AccessibleImage.css194
-rw-r--r--devtools/client/debugger/src/components/shared/AccessibleImage.js24
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.css73
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.js74
-rw-r--r--devtools/client/debugger/src/components/shared/Badge.css16
-rw-r--r--devtools/client/debugger/src/components/shared/Badge.js17
-rw-r--r--devtools/client/debugger/src/components/shared/BracketArrow.css64
-rw-r--r--devtools/client/debugger/src/components/shared/BracketArrow.js28
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CloseButton.js30
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CommandBarButton.js56
-rw-r--r--devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js61
-rw-r--r--devtools/client/debugger/src/components/shared/Button/index.js9
-rw-r--r--devtools/client/debugger/src/components/shared/Button/moz.build15
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css36
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css61
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css29
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/moz.build8
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js24
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js36
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js51
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap18
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/Dropdown.css96
-rw-r--r--devtools/client/debugger/src/components/shared/Dropdown.js71
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.css51
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.js73
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.css32
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.js299
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.css23
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.js82
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.css131
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.js82
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.css225
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.js339
-rw-r--r--devtools/client/debugger/src/components/shared/SmartGap.js166
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.css176
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.js69
-rw-r--r--devtools/client/debugger/src/components/shared/menu.css55
-rw-r--r--devtools/client/debugger/src/components/shared/moz.build23
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Accordion.spec.js40
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Badge.spec.js12
-rw-r--r--devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js19
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js16
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Modal.spec.js50
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Popover.spec.js200
-rw-r--r--devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js127
-rw-r--r--devtools/client/debugger/src/components/shared/tests/ResultList.spec.js49
-rw-r--r--devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js126
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap84
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap9
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap27
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap34
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap549
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap23
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap55
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap267
58 files changed, 4643 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.css b/devtools/client/debugger/src/components/shared/AccessibleImage.css
new file mode 100644
index 0000000000..06b8149325
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.css
@@ -0,0 +1,194 @@
+/* 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/>. */
+
+.img {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ vertical-align: middle;
+ /* use background-color for the icon color, and mask-image for its shape */
+ background-color: var(--theme-icon-color);
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ /* multicolor icons use background-image */
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ /* do not let images shrink when used as flex children */
+ flex-shrink: 0;
+}
+
+/* Expand arrow icon */
+.img.arrow {
+ width: 10px;
+ height: 10px;
+ mask-image: url(chrome://devtools/content/debugger/images/arrow.svg);
+ /* we may override the width/height in specific contexts to make the
+ clickable area bigger, but we should always keep the mask size 10x10 */
+ mask-size: 10px 10px;
+ background-color: var(--theme-icon-dimmed-color);
+ transform: rotate(-90deg);
+ transition: transform 180ms var(--animation-curve);
+}
+
+.img.arrow:dir(rtl) {
+ transform: rotate(90deg);
+}
+
+.img.arrow.expanded {
+ /* icon should always point to the bottom (default) when expanded,
+ regardless of the text direction */
+ transform: none !important;
+}
+
+.img.arrow-down {
+ mask-image: url(chrome://devtools/content/debugger/images/arrow-down.svg);
+}
+
+.img.arrow-up {
+ mask-image: url(chrome://devtools/content/debugger/images/arrow-up.svg);
+}
+
+.img.blackBox {
+ mask-image: url(chrome://devtools/content/debugger/images/blackBox.svg);
+}
+
+.img.breadcrumb {
+ mask-image: url(chrome://devtools/content/debugger/images/breadcrumbs-divider.svg);
+}
+
+.img.close {
+ mask-image: url(chrome://devtools/skin/images/close.svg);
+}
+
+.img.disable-pausing {
+ mask-image: url(chrome://devtools/content/debugger/images/disable-pausing.svg);
+}
+
+.img.enable-pausing {
+ mask-image: url(chrome://devtools/content/debugger/images/enable-pausing.svg);
+ background-color: var(--theme-icon-checked-color);
+}
+
+.img.globe {
+ mask-image: url(chrome://devtools/content/debugger/images/globe.svg);
+}
+
+.img.globe-small {
+ mask-image: url(chrome://devtools/content/debugger/images/globe-small.svg);
+ mask-size: 12px 12px;
+}
+
+.img.window {
+ mask-image: url(chrome://devtools/content/debugger/images/window.svg);
+}
+
+.img.file {
+ mask-image: url(chrome://devtools/content/debugger/images/file-small.svg);
+ mask-size: 12px 12px;
+}
+
+.img.folder {
+ mask-image: url(chrome://devtools/content/debugger/images/folder.svg);
+}
+
+.img.home {
+ mask-image: url(chrome://devtools/content/debugger/images/home.svg);
+}
+
+.img.info {
+ mask-image: url(chrome://devtools/skin/images/info.svg);
+}
+
+.img.loader {
+ background-image: url(chrome://devtools/content/debugger/images/loader.svg);
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-color);
+ background-color: unset;
+}
+
+.img.more-tabs {
+ mask-image: url(chrome://devtools/content/debugger/images/command-chevron.svg);
+}
+
+html[dir="rtl"] .img.more-tabs {
+ transform: scaleX(-1);
+}
+
+.img.next {
+ mask-image: url(chrome://devtools/content/debugger/images/next.svg);
+}
+
+.img.next-circle {
+ mask-image: url(chrome://devtools/content/debugger/images/next-circle.svg);
+}
+
+.img.pane-collapse {
+ mask-image: url(chrome://devtools/content/debugger/images/pane-collapse.svg);
+}
+
+.img.pane-expand {
+ mask-image: url(chrome://devtools/content/debugger/images/pane-expand.svg);
+}
+
+.img.pause {
+ mask-image: url(chrome://devtools/content/debugger/images/pause.svg);
+}
+
+.img.plus {
+ mask-image: url(chrome://devtools/skin/images/add.svg);
+}
+
+.img.prettyPrint {
+ background-image: url(chrome://devtools/content/debugger/images/prettyPrint.svg);
+ background-size: 14px 14px;
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.removeAll {
+ mask-image: url(chrome://devtools/skin/images/clear.svg)
+}
+
+.img.refresh {
+ mask-image: url(chrome://devtools/skin/images/reload.svg);
+}
+
+.img.resume {
+ mask-image: url(chrome://devtools/content/shared/images/resume.svg);
+}
+
+.img.search {
+ mask-image: url(chrome://devtools/content/debugger/images/search.svg);
+}
+
+.img.shortcuts {
+ mask-image: url(chrome://devtools/content/debugger/images/help.svg);
+}
+
+.img.spin {
+ animation: spin 0.5s linear infinite;
+}
+
+.img.stepIn {
+ mask-image: url(chrome://devtools/content/debugger/images/stepIn.svg);
+}
+
+.img.stepOut {
+ mask-image: url(chrome://devtools/content/debugger/images/stepOut.svg);
+}
+
+.img.stepOver {
+ mask-image: url(chrome://devtools/content/shared/images/stepOver.svg);
+}
+
+.img.tab {
+ mask-image: url(chrome://devtools/content/debugger/images/tab.svg);
+}
+
+.img.worker {
+ mask-image: url(chrome://devtools/content/debugger/images/worker.svg);
+}
diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.js b/devtools/client/debugger/src/components/shared/AccessibleImage.js
new file mode 100644
index 0000000000..1ac3510c36
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.js
@@ -0,0 +1,24 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./AccessibleImage.css";
+
+const AccessibleImage = props => {
+ props = {
+ ...props,
+ className: classnames("img", props.className),
+ };
+ return <span {...props} />;
+};
+
+AccessibleImage.propTypes = {
+ className: PropTypes.string.isRequired,
+};
+
+export default AccessibleImage;
diff --git a/devtools/client/debugger/src/components/shared/Accordion.css b/devtools/client/debugger/src/components/shared/Accordion.css
new file mode 100644
index 0000000000..e87fa41a6f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Accordion.css
@@ -0,0 +1,73 @@
+/* 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/>. */
+
+.accordion {
+ background-color: var(--theme-sidebar-background);
+ width: 100%;
+ list-style-type: none;
+ padding: 0px;
+ margin-top: 0px;
+}
+
+.accordion ._header {
+ background-color: var(--theme-accordion-header-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+ font-size: 12px;
+ line-height: calc(16 / 12);
+ padding: 4px 6px;
+ width: 100%;
+ align-items: center;
+ margin: 0px;
+ font-weight: normal;
+ cursor: default;
+ user-select: none;
+}
+
+.accordion ._header:hover {
+ background-color: var(--theme-accordion-header-hover);
+}
+
+.accordion ._header .arrow {
+ margin-inline-end: 4px;
+}
+
+.accordion ._header .header-label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--theme-toolbar-color);
+}
+
+.accordion ._header .header-buttons {
+ display: flex;
+ margin-inline-start: auto;
+}
+
+.accordion ._header .header-buttons button {
+ color: var(--theme-body-color);
+ border: none;
+ background: none;
+ padding: 0;
+ margin: 0 2px;
+ width: 16px;
+ height: 16px;
+}
+
+.accordion ._header .header-buttons button::-moz-focus-inner {
+ border: none;
+}
+
+.accordion ._header .header-buttons button .img {
+ display: block;
+}
+
+.accordion ._content {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ font-size: var(--theme-body-font-size);
+}
+
+.accordion div:last-child ._content {
+ border-bottom: none;
+}
diff --git a/devtools/client/debugger/src/components/shared/Accordion.js b/devtools/client/debugger/src/components/shared/Accordion.js
new file mode 100644
index 0000000000..fba307abaf
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Accordion.js
@@ -0,0 +1,74 @@
+/* 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/>. */
+
+import React, { cloneElement, Component } from "react";
+import PropTypes from "prop-types";
+import AccessibleImage from "./AccessibleImage";
+
+import "./Accordion.css";
+
+class Accordion extends Component {
+ static get propTypes() {
+ return {
+ items: PropTypes.array.isRequired,
+ };
+ }
+
+ handleHeaderClick(i) {
+ const item = this.props.items[i];
+ const opened = !item.opened;
+ item.opened = opened;
+
+ if (item.onToggle) {
+ item.onToggle(opened);
+ }
+
+ // We force an update because otherwise the accordion
+ // would not re-render
+ this.forceUpdate();
+ }
+
+ onHandleHeaderKeyDown(e, i) {
+ if (e && (e.key === " " || e.key === "Enter")) {
+ this.handleHeaderClick(i);
+ }
+ }
+
+ renderContainer = (item, i) => {
+ const { opened } = item;
+
+ return (
+ <li className={item.className} key={i}>
+ <h2
+ className="_header"
+ tabIndex="0"
+ onKeyDown={e => this.onHandleHeaderKeyDown(e, i)}
+ onClick={() => this.handleHeaderClick(i)}
+ >
+ <AccessibleImage className={`arrow ${opened ? "expanded" : ""}`} />
+ <span className="header-label">{item.header}</span>
+ {item.buttons ? (
+ <div className="header-buttons" tabIndex="-1">
+ {item.buttons}
+ </div>
+ ) : null}
+ </h2>
+ {opened && (
+ <div className="_content">
+ {cloneElement(item.component, item.componentProps || {})}
+ </div>
+ )}
+ </li>
+ );
+ };
+ render() {
+ return (
+ <ul className="accordion">
+ {this.props.items.map(this.renderContainer)}
+ </ul>
+ );
+ }
+}
+
+export default Accordion;
diff --git a/devtools/client/debugger/src/components/shared/Badge.css b/devtools/client/debugger/src/components/shared/Badge.css
new file mode 100644
index 0000000000..f52d32edf4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Badge.css
@@ -0,0 +1,16 @@
+/* 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/>. */
+
+.badge {
+ --size: 17px;
+ --radius: calc(var(--size) / 2);
+ height: var(--size);
+ min-width: var(--size);
+ line-height: var(--size);
+ background: var(--theme-toolbar-background-hover);
+ color: var(--theme-body-color);
+ border-radius: var(--radius);
+ padding: 0 4px;
+ font-size: 0.9em;
+}
diff --git a/devtools/client/debugger/src/components/shared/Badge.js b/devtools/client/debugger/src/components/shared/Badge.js
new file mode 100644
index 0000000000..58519e0246
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Badge.js
@@ -0,0 +1,17 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+import "./Badge.css";
+
+const Badge = ({ children }) => (
+ <span className="badge text-white text-center">{children}</span>
+);
+
+Badge.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export default Badge;
diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.css b/devtools/client/debugger/src/components/shared/BracketArrow.css
new file mode 100644
index 0000000000..afca888371
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/BracketArrow.css
@@ -0,0 +1,64 @@
+/* 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/>. */
+
+.bracket-arrow {
+ position: absolute;
+ pointer-events: none;
+}
+
+.bracket-arrow::before,
+.bracket-arrow::after {
+ content: "";
+ height: 0;
+ width: 0;
+ position: absolute;
+ border: 7px solid transparent;
+}
+
+.bracket-arrow.up::before {
+ border-bottom-color: var(--theme-splitter-color);
+ top: -1px;
+}
+
+.theme-dark .bracket-arrow.up::before {
+ border-bottom-color: var(--theme-body-color);
+}
+
+.bracket-arrow.up::after {
+ border-bottom-color: var(--theme-body-background);
+ top: 0px;
+}
+
+.bracket-arrow.down::before {
+ border-bottom-color: transparent;
+ border-top-color: var(--theme-splitter-color);
+ top: 0px;
+}
+
+.theme-dark .bracket-arrow.down::before {
+ border-top-color: var(--theme-body-color);
+}
+
+.bracket-arrow.down::after {
+ border-bottom-color: transparent;
+ border-top-color: var(--theme-body-background);
+ top: -1px;
+}
+
+.bracket-arrow.left::before {
+ border-left-color: transparent;
+ border-right-color: var(--theme-splitter-color);
+ top: 0px;
+}
+
+.theme-dark .bracket-arrow.left::before {
+ border-right-color: var(--theme-body-color);
+}
+
+.bracket-arrow.left::after {
+ border-left-color: transparent;
+ border-right-color: var(--theme-body-background);
+ top: 0px;
+ left: 1px;
+}
diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.js b/devtools/client/debugger/src/components/shared/BracketArrow.js
new file mode 100644
index 0000000000..2e0c3fbf0e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/BracketArrow.js
@@ -0,0 +1,28 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./BracketArrow.css";
+
+const BracketArrow = ({ orientation, left, top, bottom }) => {
+ return (
+ <div
+ className={classnames("bracket-arrow", orientation || "up")}
+ style={{ left, top, bottom }}
+ />
+ );
+};
+
+BracketArrow.propTypes = {
+ bottom: PropTypes.number,
+ left: PropTypes.number,
+ orientation: PropTypes.string.isRequired,
+ top: PropTypes.number,
+};
+
+export default BracketArrow;
diff --git a/devtools/client/debugger/src/components/shared/Button/CloseButton.js b/devtools/client/debugger/src/components/shared/Button/CloseButton.js
new file mode 100644
index 0000000000..2450b4aae2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/CloseButton.js
@@ -0,0 +1,30 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import AccessibleImage from "../AccessibleImage";
+
+import "./styles/CloseButton.css";
+
+function CloseButton({ handleClick, buttonClass, tooltip }) {
+ return (
+ <button
+ className={buttonClass ? `close-btn ${buttonClass}` : "close-btn"}
+ onClick={handleClick}
+ title={tooltip}
+ >
+ <AccessibleImage className="close" />
+ </button>
+ );
+}
+
+CloseButton.propTypes = {
+ buttonClass: PropTypes.string,
+ handleClick: PropTypes.func.isRequired,
+ tooltip: PropTypes.string,
+};
+
+export default CloseButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js
new file mode 100644
index 0000000000..f1579b6f7a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js
@@ -0,0 +1,56 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import AccessibleImage from "../AccessibleImage";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./styles/CommandBarButton.css";
+
+export function debugBtn(
+ onClick,
+ type,
+ className,
+ tooltip,
+ disabled = false,
+ ariaPressed = false
+) {
+ return (
+ <CommandBarButton
+ className={classnames(type, className)}
+ disabled={disabled}
+ key={type}
+ onClick={onClick}
+ pressed={ariaPressed}
+ title={tooltip}
+ >
+ <AccessibleImage className={type} />
+ </CommandBarButton>
+ );
+}
+
+const CommandBarButton = props => {
+ const { children, className, pressed = false, ...rest } = props;
+
+ return (
+ <button
+ aria-pressed={pressed}
+ className={classnames("command-bar-button", className)}
+ {...rest}
+ >
+ {children}
+ </button>
+ );
+};
+
+CommandBarButton.propTypes = {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string.isRequired,
+ pressed: PropTypes.bool,
+};
+
+export default CommandBarButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js
new file mode 100644
index 0000000000..ba2f20e882
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js
@@ -0,0 +1,61 @@
+/* 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/>. */
+
+import React, { PureComponent } from "react";
+import PropTypes from "prop-types";
+import AccessibleImage from "../AccessibleImage";
+import { CommandBarButton } from "./";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./styles/PaneToggleButton.css";
+
+class PaneToggleButton extends PureComponent {
+ static defaultProps = {
+ horizontal: false,
+ position: "start",
+ };
+
+ static get propTypes() {
+ return {
+ collapsed: PropTypes.bool.isRequired,
+ handleClick: PropTypes.func.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ position: PropTypes.oneOf(["start", "end"]).isRequired,
+ };
+ }
+
+ label(position, collapsed) {
+ switch (position) {
+ case "start":
+ return L10N.getStr(collapsed ? "expandSources" : "collapseSources");
+ case "end":
+ return L10N.getStr(
+ collapsed ? "expandBreakpoints" : "collapseBreakpoints"
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const { position, collapsed, horizontal, handleClick } = this.props;
+
+ return (
+ <CommandBarButton
+ className={classnames("toggle-button", position, {
+ collapsed,
+ vertical: !horizontal,
+ })}
+ onClick={() => handleClick(position, !collapsed)}
+ title={this.label(position, collapsed)}
+ >
+ <AccessibleImage
+ className={collapsed ? "pane-expand" : "pane-collapse"}
+ />
+ </CommandBarButton>
+ );
+ }
+}
+
+export default PaneToggleButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/index.js b/devtools/client/debugger/src/components/shared/Button/index.js
new file mode 100644
index 0000000000..df7976ba90
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/index.js
@@ -0,0 +1,9 @@
+/* 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/>. */
+
+import CloseButton from "./CloseButton";
+import CommandBarButton, { debugBtn } from "./CommandBarButton";
+import PaneToggleButton from "./PaneToggleButton";
+
+export { CloseButton, CommandBarButton, debugBtn, PaneToggleButton };
diff --git a/devtools/client/debugger/src/components/shared/Button/moz.build b/devtools/client/debugger/src/components/shared/Button/moz.build
new file mode 100644
index 0000000000..c6e652d5dc
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/moz.build
@@ -0,0 +1,15 @@
+# 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/.
+
+DIRS += [
+ "styles",
+]
+
+CompiledModules(
+ "CloseButton.js",
+ "CommandBarButton.js",
+ "index.js",
+ "PaneToggleButton.js",
+)
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css
new file mode 100644
index 0000000000..b0093ff4de
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css
@@ -0,0 +1,36 @@
+/* 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/>. */
+
+.close-btn {
+ width: 16px;
+ height: 16px;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ padding: 1px;
+ color: var(--theme-icon-color);
+}
+
+.close-btn:hover,
+.close-btn:focus {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+.close-btn .img {
+ display: block;
+ width: 12px;
+ height: 12px;
+ /* inherit the button's text color for the icon's color */
+ background-color: currentColor;
+}
+
+.close-btn.big {
+ width: 20px;
+ height: 20px;
+}
+
+.close-btn.big .img {
+ width: 16px;
+ height: 16px;
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
new file mode 100644
index 0000000000..5b03bca8ec
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
@@ -0,0 +1,61 @@
+/* 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/>. */
+
+.command-bar-button {
+ appearance: none;
+ background: transparent;
+ border: none;
+ display: inline-block;
+ text-align: center;
+ position: relative;
+ padding: 0px 5px;
+ fill: currentColor;
+ min-width: 30px;
+}
+
+.command-bar-button:disabled {
+ opacity: 0.6;
+ cursor: default;
+}
+
+.command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+.theme-dark .command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover {
+ background: var(--theme-toolbar-hover);
+}
+
+:root.theme-dark .command-bar-button {
+ color: var(--theme-body-color);
+}
+
+.command-bar-button > * {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/**
+ * Settings icon and menu
+ */
+.devtools-button.debugger-settings-menu-button {
+ border-radius: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.devtools-button.debugger-settings-menu-button::before {
+ background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
+.devtools-button.debugger-trace-menu-button::before {
+ background-image: url(chrome://devtools/content/debugger/images/trace.svg);
+}
+.devtools-button.debugger-trace-menu-button.active::before {
+ fill: var(--theme-icon-checked-color);
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css
new file mode 100644
index 0000000000..d8a2495408
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css
@@ -0,0 +1,29 @@
+/* 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/>. */
+
+.toggle-button {
+ padding: 4px 6px;
+}
+
+.toggle-button .img {
+ vertical-align: middle;
+}
+
+.toggle-button.end {
+ margin-inline-end: 0px;
+ margin-inline-start: auto;
+}
+
+.toggle-button.start {
+ margin-inline-start: 0px;
+}
+
+html[dir="rtl"] .toggle-button.start .img,
+html[dir="ltr"] .toggle-button.end:not(.vertical) .img {
+ transform: scaleX(-1);
+}
+
+.toggle-button.end.vertical .img {
+ transform: rotate(-90deg);
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/moz.build b/devtools/client/debugger/src/components/shared/Button/styles/moz.build
new file mode 100644
index 0000000000..7d80140dbe
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DIRS += []
+
+CompiledModules()
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js
new file mode 100644
index 0000000000..cb426ddada
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js
@@ -0,0 +1,24 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import { CloseButton } from "../";
+
+describe("CloseButton", () => {
+ it("renders with tooltip", () => {
+ const tooltip = "testTooltip";
+ const wrapper = shallow(
+ <CloseButton tooltip={tooltip} handleClick={() => {}} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles click event", () => {
+ const handleClickSpy = jest.fn();
+ const wrapper = shallow(<CloseButton handleClick={handleClickSpy} />);
+ wrapper.simulate("click");
+ expect(handleClickSpy).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js
new file mode 100644
index 0000000000..1da7dc9fed
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js
@@ -0,0 +1,36 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import { CommandBarButton, debugBtn } from "../";
+
+describe("CommandBarButton", () => {
+ it("renders", () => {
+ const wrapper = shallow(<CommandBarButton children={[]} className={""} />);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders children", () => {
+ const children = [1, 2, 3, 4];
+ const wrapper = shallow(
+ <CommandBarButton children={children} className={""} />
+ );
+ expect(wrapper.find("button").children()).toHaveLength(4);
+ });
+});
+
+describe("debugBtn", () => {
+ it("renders", () => {
+ const wrapper = shallow(debugBtn());
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles onClick", () => {
+ const onClickSpy = jest.fn();
+ const wrapper = shallow(debugBtn(onClickSpy));
+ wrapper.simulate("click");
+ expect(onClickSpy).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js
new file mode 100644
index 0000000000..59fbe11fc6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js
@@ -0,0 +1,51 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import { PaneToggleButton } from "../";
+
+describe("PaneToggleButton", () => {
+ const handleClickSpy = jest.fn();
+ const wrapper = shallow(
+ <PaneToggleButton
+ handleClick={handleClickSpy}
+ collapsed={false}
+ position="start"
+ />
+ );
+
+ it("renders default", () => {
+ expect(wrapper.hasClass("vertical")).toBe(true);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("toggles horizontal class", () => {
+ wrapper.setProps({ horizontal: true });
+ expect(wrapper.hasClass("vertical")).toBe(false);
+ });
+
+ it("toggles collapsed class", () => {
+ wrapper.setProps({ collapsed: true });
+ expect(wrapper.hasClass("collapsed")).toBe(true);
+ });
+
+ it("toggles start position", () => {
+ wrapper.setProps({ position: "start" });
+ expect(wrapper.hasClass("start")).toBe(true);
+ });
+
+ it("toggles end position ", () => {
+ wrapper.setProps({ position: "end" });
+ expect(wrapper.hasClass("end")).toBe(true);
+ });
+
+ it("handleClick is called", () => {
+ const position = "end";
+ const collapsed = false;
+ wrapper.setProps({ position, collapsed });
+ wrapper.simulate("click");
+ expect(handleClickSpy).toHaveBeenCalledWith(position, true);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap
new file mode 100644
index 0000000000..d0a0cb9967
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CloseButton renders with tooltip 1`] = `
+<button
+ className="close-btn"
+ onClick={[Function]}
+ title="testTooltip"
+>
+ <AccessibleImage
+ className="close"
+ />
+</button>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap
new file mode 100644
index 0000000000..cebcb5892c
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CommandBarButton renders 1`] = `
+<button
+ aria-pressed={false}
+ className="command-bar-button"
+/>
+`;
+
+exports[`debugBtn renders 1`] = `
+<button
+ aria-pressed={false}
+ className="command-bar-button"
+ disabled={false}
+>
+ <AccessibleImage />
+</button>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap
new file mode 100644
index 0000000000..86067066a6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PaneToggleButton renders default 1`] = `
+<CommandBarButton
+ className="toggle-button start vertical"
+ onClick={[Function]}
+ title="Collapse Sources and Outline panes"
+>
+ <AccessibleImage
+ className="pane-collapse"
+ />
+</CommandBarButton>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Dropdown.css b/devtools/client/debugger/src/components/shared/Dropdown.css
new file mode 100644
index 0000000000..bae5656c8f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Dropdown.css
@@ -0,0 +1,96 @@
+/* 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/>. */
+
+.dropdown {
+ background: var(--theme-body-background);
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 4px;
+ box-shadow: 0 4px 4px 0 var(--search-overlays-semitransparent);
+ max-height: 300px;
+ position: absolute;
+ top: 24px;
+ width: 150px;
+ z-index: 1000;
+ overflow: auto;
+}
+
+[dir="ltr"] .dropdown {
+ right: 2px;
+}
+
+[dir="rtl"] .dropdown {
+ left: 2px;
+}
+
+.dropdown-block {
+ position: relative;
+ align-self: center;
+ height: 100%;
+}
+
+/* cover the reserved space at the end of .source-tabs */
+.source-tabs + .dropdown-block {
+ margin-inline-start: -28px;
+}
+
+.dropdown-button {
+ color: var(--theme-comment);
+ background: none;
+ border: none;
+ padding: 4px 6px;
+ font-weight: 100;
+ font-size: 14px;
+ height: 100%;
+ width: 28px;
+}
+
+.dropdown-button .img {
+ display: block;
+}
+
+.dropdown ul {
+ margin: 0;
+ padding: 4px 0;
+ list-style: none;
+}
+
+.dropdown li {
+ display: flex;
+ align-items: center;
+ padding: 6px 8px;
+ font-size: 12px;
+ line-height: calc(16 / 12);
+ transition: all 0.25s ease;
+}
+
+.dropdown li:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.dropdown-icon {
+ margin-inline-end: 4px;
+ mask-size: 13px 13px;
+}
+
+.dropdown-label {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dropdown-icon.prettyPrint,
+.dropdown-icon.blackBox {
+ background-color: var(--theme-highlight-blue);
+}
+
+.dropdown-mask {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ z-index: 999;
+ left: 0;
+ top: 0;
+}
diff --git a/devtools/client/debugger/src/components/shared/Dropdown.js b/devtools/client/debugger/src/components/shared/Dropdown.js
new file mode 100644
index 0000000000..7051cec9c5
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Dropdown.js
@@ -0,0 +1,71 @@
+/* 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/>. */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import "./Dropdown.css";
+
+export class Dropdown extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownShown: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ icon: PropTypes.node.isRequired,
+ panel: PropTypes.node.isRequired,
+ };
+ }
+
+ toggleDropdown = e => {
+ this.setState(prevState => ({
+ dropdownShown: !prevState.dropdownShown,
+ }));
+ };
+
+ renderPanel() {
+ return (
+ <div
+ className="dropdown"
+ onClick={this.toggleDropdown}
+ style={{ display: this.state.dropdownShown ? "block" : "none" }}
+ >
+ {this.props.panel}
+ </div>
+ );
+ }
+
+ renderButton() {
+ return (
+ <button className="dropdown-button" onClick={this.toggleDropdown}>
+ {this.props.icon}
+ </button>
+ );
+ }
+
+ renderMask() {
+ return (
+ <div
+ className="dropdown-mask"
+ onClick={this.toggleDropdown}
+ style={{ display: this.state.dropdownShown ? "block" : "none" }}
+ />
+ );
+ }
+
+ render() {
+ return (
+ <div className="dropdown-block">
+ {this.renderPanel()}
+ {this.renderButton()}
+ {this.renderMask()}
+ </div>
+ );
+ }
+}
+
+export default Dropdown;
diff --git a/devtools/client/debugger/src/components/shared/Modal.css b/devtools/client/debugger/src/components/shared/Modal.css
new file mode 100644
index 0000000000..072390b001
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Modal.css
@@ -0,0 +1,51 @@
+/* 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/>. */
+
+.modal-wrapper {
+ position: fixed;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ transition: z-index 200ms;
+ z-index: 100;
+}
+
+.modal {
+ display: flex;
+ width: 80%;
+ max-height: 80vh;
+ overflow-y: auto;
+ background-color: var(--theme-toolbar-background);
+ transition: transform 150ms cubic-bezier(0.07, 0.95, 0, 1);
+ box-shadow: 1px 1px 6px 1px var(--popup-shadow-color);
+}
+
+.modal.entering,
+.modal.exited {
+ transform: translateY(-101%);
+}
+
+.modal.entered,
+.modal.exiting {
+ transform: translateY(5px);
+ flex-direction: column;
+}
+
+/* This rule is active when the screen is not narrow */
+@media (min-width: 580px) {
+ .modal {
+ width: 50%;
+ }
+}
+
+@media (min-height: 340px) {
+ .modal.entered,
+ .modal.exiting {
+ transform: translateY(30px);
+ }
+}
diff --git a/devtools/client/debugger/src/components/shared/Modal.js b/devtools/client/debugger/src/components/shared/Modal.js
new file mode 100644
index 0000000000..dec65e627b
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Modal.js
@@ -0,0 +1,73 @@
+/* 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/>. */
+
+import PropTypes from "prop-types";
+import React from "react";
+import Transition from "react-transition-group/Transition";
+const classnames = require("devtools/client/shared/classnames.js");
+import "./Modal.css";
+
+export const transitionTimeout = 50;
+
+export class Modal extends React.Component {
+ static get propTypes() {
+ return {
+ additionalClass: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ handleClose: PropTypes.func.isRequired,
+ status: PropTypes.string.isRequired,
+ };
+ }
+
+ onClick = e => {
+ e.stopPropagation();
+ };
+
+ render() {
+ const { additionalClass, children, handleClose, status } = this.props;
+
+ return (
+ <div className="modal-wrapper" onClick={handleClose}>
+ <div
+ className={classnames("modal", additionalClass, status)}
+ onClick={this.onClick}
+ >
+ {children}
+ </div>
+ </div>
+ );
+ }
+}
+
+Modal.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+export default function Slide({
+ in: inProp,
+ children,
+ additionalClass,
+ handleClose,
+}) {
+ return (
+ <Transition in={inProp} timeout={transitionTimeout} appear>
+ {status => (
+ <Modal
+ status={status}
+ additionalClass={additionalClass}
+ handleClose={handleClose}
+ >
+ {children}
+ </Modal>
+ )}
+ </Transition>
+ );
+}
+
+Slide.propTypes = {
+ additionalClass: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ handleClose: PropTypes.func.isRequired,
+ in: PropTypes.bool.isRequired,
+};
diff --git a/devtools/client/debugger/src/components/shared/Popover.css b/devtools/client/debugger/src/components/shared/Popover.css
new file mode 100644
index 0000000000..5da8ea4b63
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.css
@@ -0,0 +1,32 @@
+/* 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/>. */
+
+.popover {
+ position: fixed;
+ z-index: 100;
+ --gap-size: 10px;
+ --left-offset: -55px;
+}
+
+.popover.orientation-right {
+ display: flex;
+ flex-direction: row;
+}
+
+.popover.orientation-right .gap {
+ width: var(--gap-size);
+}
+
+.popover:not(.orientation-right) .gap {
+ height: var(--gap-size);
+ margin-left: var(--left-offset);
+}
+
+.popover:not(.orientation-right) .preview-popup {
+ margin-left: var(--left-offset);
+}
+
+.popover .add-to-expression-bar {
+ margin-left: var(--left-offset);
+}
diff --git a/devtools/client/debugger/src/components/shared/Popover.js b/devtools/client/debugger/src/components/shared/Popover.js
new file mode 100644
index 0000000000..fde7d40a21
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.js
@@ -0,0 +1,299 @@
+/* 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/>. */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import BracketArrow from "./BracketArrow";
+import SmartGap from "./SmartGap";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./Popover.css";
+
+class Popover extends Component {
+ state = {
+ coords: {
+ left: 0,
+ top: 0,
+ orientation: "down",
+ targetMid: { x: 0, y: 0 },
+ },
+ };
+ firstRender = true;
+
+ static defaultProps = {
+ type: "popover",
+ };
+
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ editorRef: PropTypes.object.isRequired,
+ mouseout: PropTypes.func.isRequired,
+ target: PropTypes.object.isRequired,
+ targetPosition: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { type } = this.props;
+ this.gapHeight = this.$gap.getBoundingClientRect().height;
+ const coords =
+ type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+
+ this.firstRender = false;
+ this.startTimer();
+ }
+
+ componentWillUnmount() {
+ if (this.timerId) {
+ clearTimeout(this.timerId);
+ }
+ }
+
+ startTimer() {
+ this.timerId = setTimeout(this.onTimeout, 0);
+ }
+
+ onTimeout = () => {
+ const isHoveredOnGap = this.$gap && this.$gap.matches(":hover");
+ const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover");
+ const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover");
+ const isHoveredOnTarget = this.props.target.matches(":hover");
+
+ if (isHoveredOnGap) {
+ if (!this.wasOnGap) {
+ this.wasOnGap = true;
+ this.timerId = setTimeout(this.onTimeout, 200);
+ return;
+ }
+ this.props.mouseout();
+ return;
+ }
+
+ // Don't clear the current preview if mouse is hovered on
+ // the current preview's token (target) or the popup element
+ if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) {
+ this.wasOnGap = false;
+ this.timerId = setTimeout(this.onTimeout, 0);
+ return;
+ }
+
+ this.props.mouseout();
+ };
+
+ calculateLeft(target, editor, popover, orientation) {
+ const estimatedLeft = target.left;
+ const estimatedRight = estimatedLeft + popover.width;
+ const isOverflowingRight = estimatedRight > editor.right;
+ if (orientation === "right") {
+ return target.left + target.width;
+ }
+ if (isOverflowingRight) {
+ const adjustedLeft = editor.right - popover.width - 8;
+ return adjustedLeft;
+ }
+ return estimatedLeft;
+ }
+
+ calculateTopForRightOrientation = (target, editor, popover) => {
+ if (popover.height <= editor.height) {
+ const rightOrientationTop = target.top - popover.height / 2;
+ if (rightOrientationTop < editor.top) {
+ return editor.top - target.height;
+ }
+ const rightOrientationBottom = rightOrientationTop + popover.height;
+ if (rightOrientationBottom > editor.bottom) {
+ return editor.bottom + target.height - popover.height + this.gapHeight;
+ }
+ return rightOrientationTop;
+ }
+ return editor.top - target.height;
+ };
+
+ calculateOrientation(target, editor, popover) {
+ const estimatedBottom = target.bottom + popover.height;
+ if (editor.bottom > estimatedBottom) {
+ return "down";
+ }
+ const upOrientationTop = target.top - popover.height;
+ if (upOrientationTop > editor.top) {
+ return "up";
+ }
+
+ return "right";
+ }
+
+ calculateTop = (target, editor, popover, orientation) => {
+ if (orientation === "down") {
+ return target.bottom;
+ }
+ if (orientation === "up") {
+ return target.top - popover.height;
+ }
+
+ return this.calculateTopForRightOrientation(target, editor, popover);
+ };
+
+ getPopoverCoords() {
+ if (!this.$popover || !this.props.editorRef) {
+ return null;
+ }
+
+ const popover = this.$popover;
+ const editor = this.props.editorRef;
+ const popoverRect = popover.getBoundingClientRect();
+ const editorRect = editor.getBoundingClientRect();
+ const targetRect = this.props.targetPosition;
+ const orientation = this.calculateOrientation(
+ targetRect,
+ editorRect,
+ popoverRect
+ );
+ const top = this.calculateTop(
+ targetRect,
+ editorRect,
+ popoverRect,
+ orientation
+ );
+ const popoverLeft = this.calculateLeft(
+ targetRect,
+ editorRect,
+ popoverRect,
+ orientation
+ );
+ let targetMid;
+ if (orientation === "right") {
+ targetMid = {
+ x: -14,
+ y: targetRect.top - top - 2,
+ };
+ } else {
+ targetMid = {
+ x: targetRect.left - popoverLeft + targetRect.width / 2 - 8,
+ y: 0,
+ };
+ }
+
+ return {
+ left: popoverLeft,
+ top,
+ orientation,
+ targetMid,
+ };
+ }
+
+ getTooltipCoords() {
+ if (!this.$tooltip || !this.props.editorRef) {
+ return null;
+ }
+ const tooltip = this.$tooltip;
+ const editor = this.props.editorRef;
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const editorRect = editor.getBoundingClientRect();
+ const targetRect = this.props.targetPosition;
+ const left = this.calculateLeft(targetRect, editorRect, tooltipRect);
+ const enoughRoomForTooltipAbove =
+ targetRect.top - editorRect.top > tooltipRect.height;
+ const top = enoughRoomForTooltipAbove
+ ? targetRect.top - tooltipRect.height
+ : targetRect.bottom;
+
+ return {
+ left,
+ top,
+ orientation: enoughRoomForTooltipAbove ? "up" : "down",
+ targetMid: { x: 0, y: 0 },
+ };
+ }
+
+ getChildren() {
+ const { children } = this.props;
+ const { coords } = this.state;
+ const gap = this.getGap();
+
+ return coords.orientation === "up" ? [children, gap] : [gap, children];
+ }
+
+ getGap() {
+ if (this.firstRender) {
+ return <div className="gap" key="gap" ref={a => (this.$gap = a)} />;
+ }
+
+ return (
+ <div className="gap" key="gap" ref={a => (this.$gap = a)}>
+ <SmartGap
+ token={this.props.target}
+ preview={this.$tooltip || this.$popover}
+ type={this.props.type}
+ gapHeight={this.gapHeight}
+ coords={this.state.coords}
+ offset={this.$gap.getBoundingClientRect().left}
+ />
+ </div>
+ );
+ }
+
+ getPopoverArrow(orientation, left, top) {
+ let arrowProps = {};
+
+ if (orientation === "up") {
+ arrowProps = { orientation: "down", bottom: 10, left };
+ } else if (orientation === "down") {
+ arrowProps = { orientation: "up", top: -2, left };
+ } else {
+ arrowProps = { orientation: "left", top, left: -4 };
+ }
+
+ return <BracketArrow {...arrowProps} />;
+ }
+
+ renderPopover() {
+ const { top, left, orientation, targetMid } = this.state.coords;
+ const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y);
+
+ return (
+ <div
+ className={classnames("popover", `orientation-${orientation}`, {
+ up: orientation === "up",
+ })}
+ style={{ top, left }}
+ ref={c => (this.$popover = c)}
+ >
+ {arrow}
+ {this.getChildren()}
+ </div>
+ );
+ }
+
+ renderTooltip() {
+ const { top, left, orientation } = this.state.coords;
+ return (
+ <div
+ className={`tooltip orientation-${orientation}`}
+ style={{ top, left }}
+ ref={c => (this.$tooltip = c)}
+ >
+ {this.getChildren()}
+ </div>
+ );
+ }
+
+ render() {
+ const { type } = this.props;
+
+ if (type === "tooltip") {
+ return this.renderTooltip();
+ }
+
+ return this.renderPopover();
+ }
+}
+
+export default Popover;
diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.css b/devtools/client/debugger/src/components/shared/PreviewFunction.css
new file mode 100644
index 0000000000..bff9ce25a2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/PreviewFunction.css
@@ -0,0 +1,23 @@
+/* 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/>. */
+
+.function-signature {
+ align-self: center;
+}
+
+.function-signature .function-name {
+ color: var(--theme-highlight-blue);
+}
+
+.function-signature .param {
+ color: var(--theme-highlight-red);
+}
+
+.function-signature .paren {
+ color: var(--object-color);
+}
+
+.function-signature .comma {
+ color: var(--object-color);
+}
diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.js b/devtools/client/debugger/src/components/shared/PreviewFunction.js
new file mode 100644
index 0000000000..760a45db5d
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/PreviewFunction.js
@@ -0,0 +1,82 @@
+/* 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/>. */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { formatDisplayName } from "../../utils/pause/frames";
+
+import "./PreviewFunction.css";
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+export default class PreviewFunction extends Component {
+ static get propTypes() {
+ return {
+ func: PropTypes.object.isRequired,
+ };
+ }
+
+ renderFunctionName(func) {
+ const { l10n } = this.context;
+ const name = formatDisplayName(func, undefined, l10n);
+ return <span className="function-name">{name}</span>;
+ }
+
+ renderParams(func) {
+ const { parameterNames = [] } = func;
+
+ return parameterNames
+ .filter(Boolean)
+ .map((param, i, arr) => {
+ const elements = [
+ <span className="param" key={param}>
+ {param}
+ </span>,
+ ];
+ // if this isn't the last param, add a comma
+ if (i !== arr.length - 1) {
+ elements.push(
+ <span className="delimiter" key={i}>
+ {", "}
+ </span>
+ );
+ }
+ return elements;
+ })
+ .flat();
+ }
+
+ jumpToDefinitionButton(func) {
+ const { location } = func;
+
+ if (!location?.url || IGNORED_SOURCE_URLS.includes(location.url)) {
+ return null;
+ }
+
+ const lastIndex = location.url.lastIndexOf("/");
+ return (
+ <button
+ className="jump-definition"
+ draggable="false"
+ title={`${location.url.slice(lastIndex + 1)}:${location.line}`}
+ />
+ );
+ }
+
+ render() {
+ const { func } = this.props;
+ return (
+ <span className="function-signature">
+ {this.renderFunctionName(func)}
+ <span className="paren">(</span>
+ {this.renderParams(func)}
+ <span className="paren">)</span>
+ {this.jumpToDefinitionButton(func)}
+ </span>
+ );
+ }
+}
+
+PreviewFunction.contextTypes = { l10n: PropTypes.object };
diff --git a/devtools/client/debugger/src/components/shared/ResultList.css b/devtools/client/debugger/src/components/shared/ResultList.css
new file mode 100644
index 0000000000..037c3497d3
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.css
@@ -0,0 +1,131 @@
+/* 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/>. */
+
+.result-list {
+ list-style: none;
+ margin: 0px;
+ padding: 0px;
+ overflow: auto;
+ width: 100%;
+ background: var(--theme-body-background);
+}
+
+.result-list * {
+ user-select: none;
+}
+
+.result-list li {
+ color: var(--theme-body-color);
+ padding: 4px 8px;
+ display: flex;
+}
+
+.result-list.big li {
+ flex-direction: row;
+ align-items: center;
+ padding: 6px 8px;
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.result-list.small li {
+ justify-content: space-between;
+}
+
+.result-list li:hover {
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .result-list li:hover {
+ background: var(--grey-70);
+}
+
+.result-list li.selected {
+ background: var(--theme-accordion-header-background);
+}
+
+.result-list.small li.selected {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.result-list li .result-item-icon {
+ background-color: var(--theme-icon-dimmed-color);
+}
+
+.result-list li .icon {
+ align-self: center;
+ margin-inline-end: 14px;
+ margin-inline-start: 4px;
+}
+
+.result-list .result-item-icon {
+ display: block;
+}
+
+.result-list .selected .result-item-icon {
+ background-color: var(--theme-selection-color);
+}
+
+.result-list li .title {
+ word-break: break-all;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-90);
+}
+
+.theme-dark .result-list li .title {
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-30);
+}
+
+.result-list li.selected .title {
+ color: white;
+}
+
+.result-list.big li.selected {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.result-list.big li.selected .subtitle {
+ color: white;
+}
+
+.result-list.big li.selected .subtitle .highlight {
+ color: white;
+ font-weight: bold;
+}
+
+.result-list.big li .subtitle {
+ word-break: break-all;
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-40);
+ margin-left: 15px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.theme-dark .result-list.big li.selected .subtitle {
+ color: white;
+}
+
+.theme-dark .result-list.big li .subtitle {
+ color: var(--theme-text-color-inactive);
+}
+
+.search-bar .result-list li.selected .subtitle {
+ color: white;
+}
+
+.search-bar .result-list {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.theme-dark .result-list {
+ background-color: var(--theme-body-background);
+}
diff --git a/devtools/client/debugger/src/components/shared/ResultList.js b/devtools/client/debugger/src/components/shared/ResultList.js
new file mode 100644
index 0000000000..bb915b8f24
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.js
@@ -0,0 +1,82 @@
+/* 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/>. */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import AccessibleImage from "./AccessibleImage";
+
+const classnames = require("devtools/client/shared/classnames.js");
+
+import "./ResultList.css";
+
+export default class ResultList extends Component {
+ static defaultProps = {
+ size: "small",
+ role: "listbox",
+ };
+
+ static get propTypes() {
+ return {
+ items: PropTypes.array.isRequired,
+ role: PropTypes.oneOf(["listbox"]),
+ selectItem: PropTypes.func.isRequired,
+ selected: PropTypes.number.isRequired,
+ size: PropTypes.oneOf(["big", "small"]),
+ };
+ }
+
+ renderListItem = (item, index) => {
+ if (item.value === "/" && item.title === "") {
+ item.title = "(index)";
+ }
+
+ const { selectItem, selected } = this.props;
+ const props = {
+ onClick: event => selectItem(event, item, index),
+ key: `${item.id}${item.value}${index}`,
+ ref: String(index),
+ title: item.value,
+ "aria-labelledby": `${item.id}-title`,
+ "aria-describedby": `${item.id}-subtitle`,
+ role: "option",
+ className: classnames("result-item", {
+ selected: index === selected,
+ }),
+ };
+
+ return (
+ <li {...props}>
+ {item.icon && (
+ <div className="icon">
+ <AccessibleImage className={item.icon} />
+ </div>
+ )}
+ <div id={`${item.id}-title`} className="title">
+ {item.title}
+ </div>
+ {item.subtitle != item.title ? (
+ <div id={`${item.id}-subtitle`} className="subtitle">
+ {item.subtitle}
+ </div>
+ ) : null}
+ </li>
+ );
+ };
+
+ render() {
+ const { size, items, role } = this.props;
+
+ return (
+ <ul
+ className={classnames("result-list", size)}
+ id="result-list"
+ role={role}
+ aria-live="polite"
+ >
+ {items.map(this.renderListItem)}
+ </ul>
+ );
+ }
+}
diff --git a/devtools/client/debugger/src/components/shared/SearchInput.css b/devtools/client/debugger/src/components/shared/SearchInput.css
new file mode 100644
index 0000000000..33d217321a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.css
@@ -0,0 +1,225 @@
+/* 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/>. */
+
+.search-outline {
+ border: 1px solid var(--theme-toolbar-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ transition: border-color 200ms ease-in-out;
+ display: flex;
+ flex-direction: column;
+}
+
+.search-field {
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ min-height: 24px;
+ width: 100%;
+ background-color: var(--theme-toolbar-background);
+}
+
+.search-field .img.search {
+ --icon-mask-size: 12px;
+ --icon-inset-inline-start: 6px;
+ position: absolute;
+ z-index: 1;
+ top: calc(50% - 8px);
+ mask-size: var(--icon-mask-size);
+ background-color: var(--theme-icon-dimmed-color);
+ pointer-events: none;
+}
+
+.search-field.big .img.search {
+ --icon-mask-size: 16px;
+ --icon-inset-inline-start: 12px;
+}
+
+[dir="ltr"] .search-field .img.search {
+ left: var(--icon-inset-inline-start);
+}
+
+[dir="rtl"] .search-field .img.search {
+ right: var(--icon-inset-inline-start);
+}
+
+.search-field .img.loader {
+ width: 24px;
+ height: 24px;
+ margin-inline-end: 4px;
+}
+
+.search-field input {
+ align-self: stretch;
+ flex-grow: 1;
+ height: 24px;
+ width: 40px;
+ border: none;
+ outline: none;
+ padding: 4px;
+ padding-inline-start: 28px;
+ line-height: 16px;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--theme-body-color);
+ background-color: transparent;
+}
+
+.exclude-patterns-field {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ flex-shrink: 0;
+ min-height: 24px;
+ width: 100%;
+ background-color: var(--theme-toolbar-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ margin-top: 1px;
+}
+
+.exclude-patterns-field input:focus {
+ outline: 1px solid var(--blue-50);
+}
+
+.exclude-patterns-field label {
+ padding-inline-start: 8px;
+ padding-top: 5px;
+ padding-bottom: 3px;
+ align-self: stretch;
+ background-color: var(--theme-body-background);
+ font-size: 12px;
+}
+
+.exclude-patterns-field input {
+ align-self: stretch;
+ height: 24px;
+ border: none;
+ padding-top: 14px;
+ padding-bottom: 14px;
+ padding-inline-start: 10px;
+ line-height: 16px;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--theme-body-color);
+ background-color: transparent;
+ border-top: 1px solid var(--theme-splitter-color);
+ min-height: 24px;
+}
+
+.exclude-patterns-field input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.search-field.big input {
+ height: 40px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-inline-start: 40px;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.search-field:focus-within {
+ outline: 1px solid var(--blue-50);
+}
+
+.search-field input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.search-field-summary {
+ align-self: center;
+ padding: 2px 4px;
+ white-space: nowrap;
+ text-align: center;
+ user-select: none;
+ color: var(--theme-text-color-alt);
+ /* Avoid layout jumps when we increment the result count quickly. With tabular
+ numbers, layout will only jump between 9 and 10, 99 and 100, etc. */
+ font-variant-numeric: tabular-nums;
+}
+
+.search-field.big .search-field-summary {
+ margin-inline-end: 4px;
+}
+
+.search-field .search-nav-buttons {
+ display: flex;
+ user-select: none;
+}
+
+.search-field .search-nav-buttons .nav-btn {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ padding: 4px;
+ background: transparent;
+}
+
+.search-field .search-nav-buttons .nav-btn:hover {
+ background-color: var(--theme-toolbar-background-hover);
+}
+
+.search-field .close-btn {
+ margin-inline-end: 4px;
+}
+
+.search-field.big .close-btn {
+ margin-inline-end: 8px;
+}
+
+.search-field .close-btn::-moz-focus-inner {
+ border: none;
+}
+
+.search-buttons-bar .pipe-divider {
+ flex: none;
+ align-self: stretch;
+ width: 1px;
+ vertical-align: middle;
+ margin: 4px;
+ background-color: var(--theme-splitter-color);
+}
+
+.search-buttons-bar * {
+ user-select: none;
+}
+
+.search-buttons-bar {
+ display: flex;
+ flex-shrink: 0;
+ justify-content: flex-end;
+ align-items: center;
+ background-color: var(--theme-toolbar-background);
+ padding: 0;
+}
+
+.search-buttons-bar .search-type-toggles {
+ display: flex;
+ align-items: center;
+ max-width: 68%;
+}
+
+.search-buttons-bar .search-type-name {
+ margin: 0 4px;
+ border: none;
+ background: transparent;
+ color: var(--theme-comment);
+}
+
+.search-buttons-bar .search-type-toggles .search-type-btn.active {
+ color: var(--theme-selection-background);
+}
+
+.theme-dark .search-buttons-bar .search-type-toggles .search-type-btn.active {
+ color: white;
+}
+
+.search-buttons-bar .close-btn {
+ margin-inline-end: 3px;
+}
diff --git a/devtools/client/debugger/src/components/shared/SearchInput.js b/devtools/client/debugger/src/components/shared/SearchInput.js
new file mode 100644
index 0000000000..c07d7c86c7
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.js
@@ -0,0 +1,339 @@
+/* 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/>. */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { connect } from "../../utils/connect";
+import { CloseButton } from "./Button";
+
+import AccessibleImage from "./AccessibleImage";
+import actions from "../../actions";
+import "./SearchInput.css";
+import { getSearchOptions } from "../../selectors";
+
+const classnames = require("devtools/client/shared/classnames.js");
+const SearchModifiers = require("devtools/client/shared/components/SearchModifiers");
+
+const arrowBtn = (onClick, type, className, tooltip) => {
+ const props = {
+ className,
+ key: type,
+ onClick,
+ title: tooltip,
+ type,
+ };
+
+ return (
+ <button {...props}>
+ <AccessibleImage className={type} />
+ </button>
+ );
+};
+
+export class SearchInput extends Component {
+ static defaultProps = {
+ expanded: false,
+ hasPrefix: false,
+ selectedItemId: "",
+ size: "",
+ showClose: true,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ history: [],
+ excludePatterns: props.searchOptions.excludePatterns,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ count: PropTypes.number.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ handleClose: PropTypes.func,
+ handleNext: PropTypes.func,
+ handlePrev: PropTypes.func,
+ hasPrefix: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onHistoryScroll: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ onKeyUp: PropTypes.func,
+ placeholder: PropTypes.string,
+ query: PropTypes.string,
+ selectedItemId: PropTypes.string,
+ shouldFocus: PropTypes.bool,
+ showClose: PropTypes.bool.isRequired,
+ showExcludePatterns: PropTypes.bool.isRequired,
+ excludePatternsLabel: PropTypes.string,
+ excludePatternsPlaceholder: PropTypes.string,
+ showErrorEmoji: PropTypes.bool.isRequired,
+ size: PropTypes.string,
+ summaryMsg: PropTypes.string,
+ searchKey: PropTypes.string.isRequired,
+ searchOptions: PropTypes.object,
+ setSearchOptions: PropTypes.func,
+ showSearchModifiers: PropTypes.bool.isRequired,
+ onToggleSearchModifier: PropTypes.func,
+ };
+ }
+
+ componentDidMount() {
+ this.setFocus();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.shouldFocus && !prevProps.shouldFocus) {
+ this.setFocus();
+ }
+ }
+
+ setFocus() {
+ if (this.$input) {
+ const input = this.$input;
+ input.focus();
+
+ if (!input.value) {
+ return;
+ }
+
+ // omit prefix @:# from being selected
+ const selectStartPos = this.props.hasPrefix ? 1 : 0;
+ input.setSelectionRange(selectStartPos, input.value.length + 1);
+ }
+ }
+
+ renderArrowButtons() {
+ const { handleNext, handlePrev } = this.props;
+
+ return [
+ arrowBtn(
+ handlePrev,
+ "arrow-up",
+ classnames("nav-btn", "prev"),
+ L10N.getFormatStr("editor.searchResults.prevResult")
+ ),
+ arrowBtn(
+ handleNext,
+ "arrow-down",
+ classnames("nav-btn", "next"),
+ L10N.getFormatStr("editor.searchResults.nextResult")
+ ),
+ ];
+ }
+
+ onFocus = e => {
+ const { onFocus } = this.props;
+
+ if (onFocus) {
+ onFocus(e);
+ }
+ };
+
+ onBlur = e => {
+ const { onBlur } = this.props;
+
+ if (onBlur) {
+ onBlur(e);
+ }
+ };
+
+ onKeyDown = e => {
+ const { onHistoryScroll, onKeyDown } = this.props;
+ if (!onHistoryScroll) {
+ onKeyDown(e);
+ return;
+ }
+
+ const inputValue = e.target.value;
+ const { history } = this.state;
+ const currentHistoryIndex = history.indexOf(inputValue);
+
+ if (e.key === "Enter") {
+ this.saveEnteredTerm(inputValue);
+ onKeyDown(e);
+ return;
+ }
+
+ if (e.key === "ArrowUp") {
+ const previous =
+ currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1;
+ const previousInHistory = history[previous];
+ if (previousInHistory) {
+ e.preventDefault();
+ onHistoryScroll(previousInHistory);
+ }
+ return;
+ }
+
+ if (e.key === "ArrowDown") {
+ const next = currentHistoryIndex + 1;
+ const nextInHistory = history[next];
+ if (nextInHistory) {
+ onHistoryScroll(nextInHistory);
+ }
+ }
+ };
+
+ onExcludeKeyDown = e => {
+ if (e.key === "Enter") {
+ this.props.setSearchOptions(this.props.searchKey, {
+ excludePatterns: this.state.excludePatterns,
+ });
+ this.props.onKeyDown(e);
+ }
+ };
+
+ saveEnteredTerm(query) {
+ const { history } = this.state;
+ const previousIndex = history.indexOf(query);
+ if (previousIndex !== -1) {
+ history.splice(previousIndex, 1);
+ }
+ history.push(query);
+ this.setState({ history });
+ }
+
+ renderSummaryMsg() {
+ const { summaryMsg } = this.props;
+
+ if (!summaryMsg) {
+ return null;
+ }
+
+ return <div className="search-field-summary">{summaryMsg}</div>;
+ }
+
+ renderSpinner() {
+ const { isLoading } = this.props;
+ if (!isLoading) {
+ return null;
+ }
+ return <AccessibleImage className="loader spin" />;
+ }
+
+ renderNav() {
+ const { count, handleNext, handlePrev } = this.props;
+ if ((!handleNext && !handlePrev) || !count || count == 1) {
+ return null;
+ }
+
+ return (
+ <div className="search-nav-buttons">{this.renderArrowButtons()}</div>
+ );
+ }
+
+ renderSearchModifiers() {
+ if (!this.props.showSearchModifiers) {
+ return null;
+ }
+ return (
+ <SearchModifiers
+ modifiers={this.props.searchOptions}
+ onToggleSearchModifier={updatedOptions => {
+ this.props.setSearchOptions(this.props.searchKey, updatedOptions);
+ this.props.onToggleSearchModifier();
+ }}
+ />
+ );
+ }
+
+ renderExcludePatterns() {
+ if (!this.props.showExcludePatterns) {
+ return null;
+ }
+
+ return (
+ <div className={classnames("exclude-patterns-field", this.props.size)}>
+ <label>{this.props.excludePatternsLabel}</label>
+ <input
+ placeholder={this.props.excludePatternsPlaceholder}
+ value={this.state.excludePatterns}
+ onKeyDown={this.onExcludeKeyDown}
+ onChange={e => this.setState({ excludePatterns: e.target.value })}
+ />
+ </div>
+ );
+ }
+
+ renderClose() {
+ if (!this.props.showClose) {
+ return null;
+ }
+ return (
+ <React.Fragment>
+ <span className="pipe-divider" />
+ <CloseButton
+ handleClick={this.props.handleClose}
+ buttonClass={this.props.size}
+ />
+ </React.Fragment>
+ );
+ }
+
+ render() {
+ const {
+ expanded,
+ onChange,
+ onKeyUp,
+ placeholder,
+ query,
+ selectedItemId,
+ showErrorEmoji,
+ size,
+ } = this.props;
+
+ const inputProps = {
+ className: classnames({
+ empty: showErrorEmoji,
+ }),
+ onChange,
+ onKeyDown: e => this.onKeyDown(e),
+ onKeyUp,
+ onFocus: e => this.onFocus(e),
+ onBlur: e => this.onBlur(e),
+ "aria-autocomplete": "list",
+ "aria-controls": "result-list",
+ "aria-activedescendant":
+ expanded && selectedItemId ? `${selectedItemId}-title` : "",
+ placeholder,
+ value: query,
+ spellCheck: false,
+ ref: c => (this.$input = c),
+ };
+
+ return (
+ <div className="search-outline">
+ <div
+ className={classnames("search-field", size)}
+ role="combobox"
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ aria-expanded={expanded}
+ >
+ <AccessibleImage className="search" />
+ <input {...inputProps} />
+ {this.renderSpinner()}
+ {this.renderSummaryMsg()}
+ {this.renderNav()}
+ <div className="search-buttons-bar">
+ {this.renderSearchModifiers()}
+ {this.renderClose()}
+ </div>
+ </div>
+ {this.renderExcludePatterns()}
+ </div>
+ );
+ }
+}
+const mapStateToProps = (state, props) => ({
+ searchOptions: getSearchOptions(state, props.searchKey),
+});
+
+export default connect(mapStateToProps, {
+ setSearchOptions: actions.setSearchOptions,
+})(SearchInput);
diff --git a/devtools/client/debugger/src/components/shared/SmartGap.js b/devtools/client/debugger/src/components/shared/SmartGap.js
new file mode 100644
index 0000000000..785d7496fb
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SmartGap.js
@@ -0,0 +1,166 @@
+/* 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/>. */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+function shorten(coordinates) {
+ // In cases where the token is wider than the preview, the smartGap
+ // gets distorted. This shortens the coordinate array so that the smartGap
+ // is only touching 2 corners of the token (instead of all 4 corners)
+ coordinates.splice(0, 2);
+ coordinates.splice(4, 2);
+ return coordinates;
+}
+
+function getSmartGapCoordinates(
+ preview,
+ token,
+ offset,
+ orientation,
+ gapHeight,
+ coords
+) {
+ if (orientation === "up") {
+ const coordinates = [
+ token.left - coords.left + offset,
+ token.top + token.height - (coords.top + preview.height) + gapHeight,
+ 0,
+ 0,
+ preview.width + offset,
+ 0,
+ token.left + token.width - coords.left + offset,
+ token.top + token.height - (coords.top + preview.height) + gapHeight,
+ token.left + token.width - coords.left + offset,
+ token.top - (coords.top + preview.height) + gapHeight,
+ token.left - coords.left + offset,
+ token.top - (coords.top + preview.height) + gapHeight,
+ ];
+ return preview.width > token.width ? coordinates : shorten(coordinates);
+ }
+ if (orientation === "down") {
+ const coordinates = [
+ token.left + token.width - (coords.left + preview.top) + offset,
+ 0,
+ preview.width + offset,
+ coords.top - token.top + gapHeight,
+ 0,
+ coords.top - token.top + gapHeight,
+ token.left - (coords.left + preview.top) + offset,
+ 0,
+ token.left - (coords.left + preview.top) + offset,
+ token.height,
+ token.left + token.width - (coords.left + preview.top) + offset,
+ token.height,
+ ];
+ return preview.width > token.width ? coordinates : shorten(coordinates);
+ }
+ return [
+ 0,
+ token.top - coords.top,
+ gapHeight + token.width,
+ 0,
+ gapHeight + token.width,
+ preview.height - gapHeight,
+ 0,
+ token.top + token.height - coords.top,
+ token.width,
+ token.top + token.height - coords.top,
+ token.width,
+ token.top - coords.top,
+ ];
+}
+
+function getSmartGapDimensions(
+ previewRect,
+ tokenRect,
+ offset,
+ orientation,
+ gapHeight,
+ coords
+) {
+ if (orientation === "up") {
+ return {
+ height:
+ tokenRect.top +
+ tokenRect.height -
+ coords.top -
+ previewRect.height +
+ gapHeight,
+ width: Math.max(previewRect.width, tokenRect.width) + offset,
+ };
+ }
+ if (orientation === "down") {
+ return {
+ height: coords.top - tokenRect.top + gapHeight,
+ width: Math.max(previewRect.width, tokenRect.width) + offset,
+ };
+ }
+ return {
+ height: previewRect.height - gapHeight,
+ width: coords.left - tokenRect.left + gapHeight,
+ };
+}
+
+export default function SmartGap({
+ token,
+ preview,
+ type,
+ gapHeight,
+ coords,
+ offset,
+}) {
+ const tokenRect = token.getBoundingClientRect();
+ const previewRect = preview.getBoundingClientRect();
+ const { orientation } = coords;
+ let optionalMarginLeft, optionalMarginTop;
+
+ if (orientation === "down") {
+ optionalMarginTop = -tokenRect.height;
+ } else if (orientation === "right") {
+ optionalMarginLeft = -tokenRect.width;
+ }
+
+ const { height, width } = getSmartGapDimensions(
+ previewRect,
+ tokenRect,
+ -offset,
+ orientation,
+ gapHeight,
+ coords
+ );
+ const coordinates = getSmartGapCoordinates(
+ previewRect,
+ tokenRect,
+ -offset,
+ orientation,
+ gapHeight,
+ coords
+ );
+
+ return (
+ <svg
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{
+ height,
+ width,
+ position: "absolute",
+ marginLeft: optionalMarginLeft,
+ marginTop: optionalMarginTop,
+ }}
+ >
+ <polygon points={coordinates} fill="transparent" />
+ </svg>
+ );
+}
+
+SmartGap.propTypes = {
+ coords: PropTypes.object.isRequired,
+ gapHeight: PropTypes.number.isRequired,
+ offset: PropTypes.number.isRequired,
+ preview: PropTypes.object.isRequired,
+ token: PropTypes.object.isRequired,
+ type: PropTypes.oneOf(["popover", "tooltip"]).isRequired,
+};
diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.css b/devtools/client/debugger/src/components/shared/SourceIcon.css
new file mode 100644
index 0000000000..0b9bf3e79e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.css
@@ -0,0 +1,176 @@
+/* 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/>. */
+
+/**
+ * Variant of AccessibleImage used in sources list and tabs.
+ * Define the different source type / framework / library icons here.
+ */
+
+.source-icon {
+ margin-inline-end: 4px;
+}
+
+/* Icons for frameworks and libs */
+
+.img.aframe {
+ background-image: url(chrome://devtools/content/debugger/images/sources/aframe.svg);
+ background-color: transparent !important;
+}
+
+.img.angular {
+ background-image: url(chrome://devtools/content/debugger/images/sources/angular.svg);
+ background-color: transparent !important;
+}
+
+.img.babel {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/babel.svg);
+}
+
+.img.backbone {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/backbone.svg);
+}
+
+.img.choo {
+ background-image: url(chrome://devtools/content/debugger/images/sources/choo.svg);
+ background-color: transparent !important;
+}
+
+.img.coffeescript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/coffeescript.svg);
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.dojo {
+ background-image: url(chrome://devtools/content/debugger/images/sources/dojo.svg);
+ background-color: transparent !important;
+}
+
+.img.ember {
+ background-image: url(chrome://devtools/content/debugger/images/sources/ember.svg);
+ background-color: transparent !important;
+}
+
+.img.express {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/express.svg);
+}
+
+.img.extension {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/extension.svg);
+}
+
+.img.immutable {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/immutable.svg);
+}
+
+.img.javascript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/javascript.svg);
+ background-size: 14px 14px;
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.override::after {
+ content: "";
+ display: block;
+ height: 5px;
+ width: 5px;
+ background-color: var(--purple-30);
+ border-radius: 100%;
+ outline: 1px solid var(--theme-sidebar-background);
+ translate: 12px 10px;
+}
+
+.node.focused .img.override::after {
+ outline-color: var(--theme-selection-background);
+}
+
+.img.jquery {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/jquery.svg);
+}
+
+.img.lodash {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/lodash.svg);
+}
+
+.img.marko {
+ background-image: url(chrome://devtools/content/debugger/images/sources/marko.svg);
+ background-color: transparent !important;
+}
+
+.img.mobx {
+ background-image: url(chrome://devtools/content/debugger/images/sources/mobx.svg);
+ background-color: transparent !important;
+}
+
+.img.nextjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/nextjs.svg);
+ background-color: transparent !important;
+}
+
+.img.node {
+ background-image: url(chrome://devtools/content/debugger/images/sources/node.svg);
+ background-color: transparent !important;
+}
+
+.img.nuxtjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/nuxtjs.svg);
+ background-color: transparent !important;
+}
+
+.img.preact {
+ background-image: url(chrome://devtools/content/debugger/images/sources/preact.svg);
+ background-color: transparent !important;
+}
+
+.img.pug {
+ background-image: url(chrome://devtools/content/debugger/images/sources/pug.svg);
+ background-color: transparent !important;
+}
+
+.img.react {
+ background-image: url(chrome://devtools/content/debugger/images/sources/react.svg);
+ background-color: transparent !important;
+ fill: var(--theme-highlight-bluegrey);
+ -moz-context-properties: fill;
+}
+
+.img.redux {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/redux.svg);
+}
+
+.img.rxjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/rxjs.svg);
+ background-color: transparent !important;
+}
+
+.img.sencha-extjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/sencha-extjs.svg);
+ background-color: transparent !important;
+}
+
+.img.typescript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/typescript.svg);
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.underscore {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/underscore.svg);
+}
+
+/* We use both 'Vue' and 'VueJS' when identifying frameworks */
+.img.vue,
+.img.vuejs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/vuejs.svg);
+ background-color: transparent !important;
+}
+
+.img.webpack {
+ background-image: url(chrome://devtools/content/debugger/images/sources/webpack.svg);
+ background-color: transparent !important;
+}
diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.js b/devtools/client/debugger/src/components/shared/SourceIcon.js
new file mode 100644
index 0000000000..fed2e01f57
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.js
@@ -0,0 +1,69 @@
+/* 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/>. */
+
+import React, { PureComponent } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "../../utils/connect";
+
+import AccessibleImage from "./AccessibleImage";
+
+import { getSourceClassnames } from "../../utils/source";
+import { getSymbols, isSourceBlackBoxed, hasPrettyTab } from "../../selectors";
+
+import "./SourceIcon.css";
+
+class SourceIcon extends PureComponent {
+ static get propTypes() {
+ return {
+ modifier: PropTypes.func.isRequired,
+ location: PropTypes.object.isRequired,
+ iconClass: PropTypes.string,
+ forTab: PropTypes.bool,
+ };
+ }
+
+ render() {
+ const { modifier } = this.props;
+ let { iconClass } = this.props;
+
+ if (modifier) {
+ const modified = modifier(iconClass);
+ if (!modified) {
+ return null;
+ }
+ iconClass = modified;
+ }
+
+ return <AccessibleImage className={`source-icon ${iconClass}`} />;
+ }
+}
+
+export default connect((state, props) => {
+ const { forTab, location } = props;
+ // BreakpointHeading sometimes spawn locations without source actor for generated sources
+ // which disallows fetching symbols. In such race condition return the default icon.
+ // (this reproduces when running browser_dbg-breakpoints-popup.js)
+ if (!location.source.isOriginal && !location.sourceActor) {
+ return "file";
+ }
+ const symbols = getSymbols(state, location);
+ const isBlackBoxed = isSourceBlackBoxed(state, location.source);
+ // For the tab icon, we don't want to show the pretty icon for the non-pretty tab
+ const hasMatchingPrettyTab =
+ !forTab && hasPrettyTab(state, location.source.url);
+
+ // This is the key function that will compute the icon type,
+ // In addition to the "modifier" implemented by each callsite.
+ const iconClass = getSourceClassnames(
+ location.source,
+ symbols,
+ isBlackBoxed,
+ hasMatchingPrettyTab
+ );
+
+ return {
+ iconClass,
+ };
+})(SourceIcon);
diff --git a/devtools/client/debugger/src/components/shared/menu.css b/devtools/client/debugger/src/components/shared/menu.css
new file mode 100644
index 0000000000..37dfbc2e8f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/menu.css
@@ -0,0 +1,55 @@
+/* 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/>. */
+
+menupopup {
+ position: fixed;
+ z-index: 10000;
+ border: 1px solid #cccccc;
+ padding: 5px 0;
+ background: #f2f2f2;
+ border-radius: 5px;
+ color: #585858;
+ box-shadow: 0 0 4px 0 rgba(190, 190, 190, 0.8);
+ min-width: 130px;
+}
+
+menuitem {
+ display: block;
+ padding: 0 20px;
+ line-height: 20px;
+ font-weight: 500;
+ font-size: 13px;
+ user-select: none;
+}
+
+menuitem:hover {
+ background: #3780fb;
+ color: white;
+}
+
+menuitem[disabled="true"] {
+ color: #cccccc;
+}
+
+menuitem[disabled="true"]:hover {
+ background-color: transparent;
+ cursor: default;
+}
+
+menuseparator {
+ border-bottom: 1px solid #cacdd3;
+ width: 100%;
+ height: 5px;
+ display: block;
+ margin-bottom: 5px;
+}
+
+#contextmenu-mask.show {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+}
diff --git a/devtools/client/debugger/src/components/shared/moz.build b/devtools/client/debugger/src/components/shared/moz.build
new file mode 100644
index 0000000000..b30ea0ab4f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/moz.build
@@ -0,0 +1,23 @@
+# 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/.
+
+DIRS += [
+ "Button",
+]
+
+CompiledModules(
+ "AccessibleImage.js",
+ "Accordion.js",
+ "Badge.js",
+ "BracketArrow.js",
+ "Dropdown.js",
+ "Modal.js",
+ "Popover.js",
+ "PreviewFunction.js",
+ "ResultList.js",
+ "SearchInput.js",
+ "SourceIcon.js",
+ "SmartGap.js",
+)
diff --git a/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js
new file mode 100644
index 0000000000..c15dbb827c
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js
@@ -0,0 +1,40 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import Accordion from "../Accordion";
+
+describe("Accordion", () => {
+ const testItems = [
+ {
+ header: "Test Accordion Item 1",
+ className: "accordion-item-1",
+ component: <div />,
+ opened: false,
+ onToggle: jest.fn(),
+ },
+ {
+ header: "Test Accordion Item 2",
+ className: "accordion-item-2",
+ component: <div />,
+ buttons: <button />,
+ opened: false,
+ onToggle: jest.fn(),
+ },
+ {
+ header: "Test Accordion Item 3",
+ className: "accordion-item-3",
+ component: <div />,
+ opened: true,
+ onToggle: jest.fn(),
+ },
+ ];
+ const wrapper = shallow(<Accordion items={testItems} />);
+ it("basic render", () => expect(wrapper).toMatchSnapshot());
+ wrapper.find(".accordion-item-1 ._header").simulate("click");
+ it("handleClick and onToggle", () =>
+ expect(testItems[0].onToggle).toHaveBeenCalledWith(true));
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Badge.spec.js b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js
new file mode 100644
index 0000000000..6a10b7f9e4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js
@@ -0,0 +1,12 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import Badge from "../Badge";
+
+describe("Badge", () => {
+ it("render", () => expect(shallow(<Badge>{3}</Badge>)).toMatchSnapshot());
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js
new file mode 100644
index 0000000000..37f58fbfdc
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js
@@ -0,0 +1,19 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import BracketArrow from "../BracketArrow";
+
+describe("BracketArrow", () => {
+ const wrapper = shallow(
+ <BracketArrow orientation="down" left={10} top={20} bottom={50} />
+ );
+ it("render", () => expect(wrapper).toMatchSnapshot());
+ it("render up", () => {
+ wrapper.setProps({ orientation: null });
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js
new file mode 100644
index 0000000000..b01f6fa059
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js
@@ -0,0 +1,16 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import Dropdown from "../Dropdown";
+
+describe("Dropdown", () => {
+ const wrapper = shallow(<Dropdown panel={<div />} icon="✅" />);
+ it("render", () => expect(wrapper).toMatchSnapshot());
+ wrapper.find(".dropdown").simulate("click");
+ it("handle toggleDropdown", () =>
+ expect(wrapper.state().dropdownShown).toEqual(true));
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Modal.spec.js b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js
new file mode 100644
index 0000000000..d609d3fda0
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js
@@ -0,0 +1,50 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import { Modal } from "../Modal";
+
+describe("Modal", () => {
+ it("renders", () => {
+ const wrapper = shallow(<Modal handleClose={() => {}} status="entering" />);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles close modal click", () => {
+ const handleCloseSpy = jest.fn();
+ const wrapper = shallow(
+ <Modal handleClose={handleCloseSpy} status="entering" />
+ );
+ wrapper.find(".modal-wrapper").simulate("click");
+ expect(handleCloseSpy).toHaveBeenCalled();
+ });
+
+ it("renders children", () => {
+ const children = <div className="aChild" />;
+ const wrapper = shallow(
+ <Modal children={children} handleClose={() => {}} status="entering" />
+ );
+ expect(wrapper.find(".aChild")).toHaveLength(1);
+ });
+
+ it("passes additionalClass to child div class", () => {
+ const additionalClass = "testAddon";
+ const wrapper = shallow(
+ <Modal
+ additionalClass={additionalClass}
+ handleClose={() => {}}
+ status="entering"
+ />
+ );
+ expect(wrapper.find(`.modal-wrapper .${additionalClass}`)).toHaveLength(1);
+ });
+
+ it("passes status to child div class", () => {
+ const status = "testStatus";
+ const wrapper = shallow(<Modal status={status} handleClose={() => {}} />);
+ expect(wrapper.find(`.modal-wrapper .${status}`)).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Popover.spec.js b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js
new file mode 100644
index 0000000000..fb44f16597
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js
@@ -0,0 +1,200 @@
+/* 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/>. */
+
+import React from "react";
+import { mount } from "enzyme";
+
+import Popover from "../Popover";
+
+describe("Popover", () => {
+ const onMouseLeave = jest.fn();
+ const onKeyDown = jest.fn();
+ const editorRef = {
+ getBoundingClientRect() {
+ return {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 250,
+ right: 0,
+ bottom: 0,
+ left: 20,
+ };
+ },
+ };
+
+ const targetRef = {
+ getBoundingClientRect() {
+ return {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 250,
+ right: 0,
+ bottom: 0,
+ left: 20,
+ };
+ },
+ };
+ const targetPosition = {
+ x: 100,
+ y: 200,
+ width: 300,
+ height: 300,
+ top: 50,
+ right: 0,
+ bottom: 0,
+ left: 200,
+ };
+ const popover = mount(
+ <Popover
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editorRef}
+ targetPosition={targetPosition}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Poppy!</h1>
+ </Popover>
+ );
+
+ const tooltip = mount(
+ <Popover
+ type="tooltip"
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editorRef}
+ targetPosition={targetPosition}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Toolie!</h1>
+ </Popover>
+ );
+
+ beforeEach(() => {
+ onMouseLeave.mockClear();
+ onKeyDown.mockClear();
+ });
+
+ it("render", () => expect(popover).toMatchSnapshot());
+
+ it("render (tooltip)", () => expect(tooltip).toMatchSnapshot());
+
+ it("mount popover", () => {
+ const mountedPopover = mount(
+ <Popover
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editorRef}
+ targetPosition={targetPosition}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Poppy!</h1>
+ </Popover>
+ );
+ expect(mountedPopover).toMatchSnapshot();
+ });
+
+ it("mount tooltip", () => {
+ const mountedTooltip = mount(
+ <Popover
+ type="tooltip"
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editorRef}
+ targetPosition={targetPosition}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Toolie!</h1>
+ </Popover>
+ );
+ expect(mountedTooltip).toMatchSnapshot();
+ });
+
+ it("tooltip normally displays above the target", () => {
+ const editor = {
+ getBoundingClientRect() {
+ return {
+ width: 500,
+ height: 500,
+ top: 0,
+ bottom: 500,
+ left: 0,
+ right: 500,
+ };
+ },
+ };
+ const target = {
+ width: 30,
+ height: 10,
+ top: 100,
+ bottom: 110,
+ left: 20,
+ right: 50,
+ };
+
+ const mountedTooltip = mount(
+ <Popover
+ type="tooltip"
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editor}
+ targetPosition={target}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Toolie!</h1>
+ </Popover>
+ );
+
+ const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10);
+ expect(toolTipTop).toBeLessThanOrEqual(target.top);
+ });
+
+ it("tooltop won't display above the target when insufficient space", () => {
+ const editor = {
+ getBoundingClientRect() {
+ return {
+ width: 100,
+ height: 100,
+ top: 0,
+ bottom: 100,
+ left: 0,
+ right: 100,
+ };
+ },
+ };
+ const target = {
+ width: 30,
+ height: 10,
+ top: 0,
+ bottom: 10,
+ left: 20,
+ right: 50,
+ };
+
+ const mountedTooltip = mount(
+ <Popover
+ type="tooltip"
+ onMouseLeave={onMouseLeave}
+ onKeyDown={onKeyDown}
+ editorRef={editor}
+ targetPosition={target}
+ mouseout={() => {}}
+ target={targetRef}
+ >
+ <h1>Toolie!</h1>
+ </Popover>
+ );
+
+ const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10);
+ expect(toolTipTop).toBeGreaterThanOrEqual(target.bottom);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js
new file mode 100644
index 0000000000..391e5628df
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js
@@ -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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import PreviewFunction from "../PreviewFunction";
+
+function render(props) {
+ return shallow(<PreviewFunction {...props} />, { context: { l10n: L10N } });
+}
+
+describe("PreviewFunction", () => {
+ it("should return a span", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan).toMatchSnapshot();
+ expect(returnedSpan.name()).toEqual("span");
+ });
+
+ it('should return a span with a class of "function-signature"', () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.hasClass("function-signature")).toBe(true);
+ });
+
+ it("should return a span with 3 children", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children()).toHaveLength(3);
+ });
+
+ describe("function name", () => {
+ it("should be a span", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().name()).toEqual("span");
+ });
+
+ it('should have a "function-name" class', () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().hasClass("function-name")).toBe(
+ true
+ );
+ });
+
+ it("should be be set to userDisplayName if defined", () => {
+ const item = {
+ name: "",
+ userDisplayName: "chuck",
+ displayName: "norris",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("chuck");
+ });
+
+ it('should use displayName if defined & no "userDisplayName" exist', () => {
+ const item = {
+ displayName: "norris",
+ name: "last",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("norris");
+ });
+
+ it('should use to name if no "userDisplayName"/"displayName" exist', () => {
+ const item = {
+ name: "last",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("last");
+ });
+ });
+
+ describe("render parentheses", () => {
+ let leftParen;
+ let rightParen;
+
+ beforeAll(() => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ const children = returnedSpan.children();
+ leftParen = returnedSpan.childAt(1);
+ rightParen = returnedSpan.childAt(children.length - 1);
+ });
+
+ it("should be spans", () => {
+ expect(leftParen.name()).toEqual("span");
+ expect(rightParen.name()).toEqual("span");
+ });
+
+ it("should create a left paren", () => {
+ expect(leftParen.text()).toEqual("(");
+ });
+
+ it("should create a right paren", () => {
+ expect(rightParen.text()).toEqual(")");
+ });
+ });
+
+ describe("render parameters", () => {
+ let returnedSpan;
+ let children;
+
+ beforeAll(() => {
+ const item = {
+ name: "",
+ parameterNames: ["one", "two", "three"],
+ };
+ returnedSpan = render({ func: item });
+ children = returnedSpan.children();
+ });
+
+ it("should render spans according to the dynamic params given", () => {
+ expect(children).toHaveLength(8);
+ });
+
+ it("should render the parameters names", () => {
+ expect(returnedSpan.childAt(2).text()).toEqual("one");
+ });
+
+ it("should render the parameters commas", () => {
+ expect(returnedSpan.childAt(3).text()).toEqual(", ");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js
new file mode 100644
index 0000000000..2751f3abd6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js
@@ -0,0 +1,49 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import ResultList from "../ResultList";
+
+const selectItem = jest.fn();
+const selectedIndex = 1;
+const payload = {
+ items: [
+ {
+ id: 0,
+ subtitle: "subtitle",
+ title: "title",
+ value: "value",
+ },
+ {
+ id: 1,
+ subtitle: "subtitle 1",
+ title: "title 1",
+ value: "value 1",
+ },
+ ],
+ selected: selectedIndex,
+ selectItem,
+};
+
+describe("Result list", () => {
+ it("should call onClick function", () => {
+ const wrapper = shallow(<ResultList {...payload} />);
+
+ wrapper.childAt(selectedIndex).simulate("click");
+ expect(selectItem).toHaveBeenCalled();
+ });
+
+ it("should render the component", () => {
+ const wrapper = shallow(<ResultList {...payload} />);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("selected index should have 'selected class'", () => {
+ const wrapper = shallow(<ResultList {...payload} />);
+ const childHasClass = wrapper.childAt(selectedIndex).hasClass("selected");
+
+ expect(childHasClass).toEqual(true);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js
new file mode 100644
index 0000000000..c0fff81b24
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js
@@ -0,0 +1,126 @@
+/* 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/>. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import configureStore from "redux-mock-store";
+
+import SearchInput from "../SearchInput";
+
+describe("SearchInput", () => {
+ // !! wrapper is defined outside test scope
+ // so it will keep values between tests
+ const mockStore = configureStore([]);
+ const store = mockStore({
+ ui: { mutableSearchOptions: { "foo-search": {} } },
+ });
+ const wrapper = shallow(
+ <SearchInput
+ store={store}
+ query=""
+ count={5}
+ placeholder="A placeholder"
+ summaryMsg="So many results"
+ showErrorEmoji={false}
+ isLoading={false}
+ onChange={() => {}}
+ onKeyDown={() => {}}
+ searchKey="foo-search"
+ showSearchModifiers={false}
+ showExcludePatterns={false}
+ showClose={true}
+ handleClose={jest.fn()}
+ setSearchOptions={jest.fn()}
+ />
+ ).dive();
+
+ it("renders", () => expect(wrapper).toMatchSnapshot());
+
+ it("shows nav buttons", () => {
+ wrapper.setProps({
+ handleNext: jest.fn(),
+ handlePrev: jest.fn(),
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("shows svg error emoji", () => {
+ wrapper.setProps({ showErrorEmoji: true });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("shows svg magnifying glass", () => {
+ wrapper.setProps({ showErrorEmoji: false });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ describe("with optional onHistoryScroll", () => {
+ const searches = ["foo", "bar", "baz"];
+ const createSearch = term => ({
+ target: { value: term },
+ key: "Enter",
+ });
+
+ const scrollUp = currentTerm => ({
+ key: "ArrowUp",
+ target: { value: currentTerm },
+ preventDefault: jest.fn(),
+ });
+ const scrollDown = currentTerm => ({
+ key: "ArrowDown",
+ target: { value: currentTerm },
+ preventDefault: jest.fn(),
+ });
+
+ it("stores entered history in state", () => {
+ wrapper.setProps({
+ onHistoryScroll: jest.fn(),
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ });
+
+ it("stores scroll history in state", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ expect(wrapper.state().history[1]).toEqual(searches[1]);
+ });
+
+ it("scrolls up stored history on arrow up", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[1]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ expect(wrapper.state().history[1]).toEqual(searches[1]);
+ expect(onHistoryScroll).toHaveBeenCalledWith(searches[0]);
+ });
+
+ it("scrolls down stored history on arrow down", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[2]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[2]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[1]));
+ wrapper.find("input").simulate("keyDown", scrollDown(searches[0]));
+ expect(onHistoryScroll.mock.calls[2][0]).toBe(searches[1]);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap
new file mode 100644
index 0000000000..7ab4ed1ee6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Accordion basic render 1`] = `
+<ul
+ className="accordion"
+>
+ <li
+ className="accordion-item-1"
+ key="0"
+ >
+ <h2
+ className="_header"
+ onClick={[Function]}
+ onKeyDown={[Function]}
+ tabIndex="0"
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ <span
+ className="header-label"
+ >
+ Test Accordion Item 1
+ </span>
+ </h2>
+ <div
+ className="_content"
+ >
+ <div />
+ </div>
+ </li>
+ <li
+ className="accordion-item-2"
+ key="1"
+ >
+ <h2
+ className="_header"
+ onClick={[Function]}
+ onKeyDown={[Function]}
+ tabIndex="0"
+ >
+ <AccessibleImage
+ className="arrow "
+ />
+ <span
+ className="header-label"
+ >
+ Test Accordion Item 2
+ </span>
+ <div
+ className="header-buttons"
+ tabIndex="-1"
+ >
+ <button />
+ </div>
+ </h2>
+ </li>
+ <li
+ className="accordion-item-3"
+ key="2"
+ >
+ <h2
+ className="_header"
+ onClick={[Function]}
+ onKeyDown={[Function]}
+ tabIndex="0"
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ <span
+ className="header-label"
+ >
+ Test Accordion Item 3
+ </span>
+ </h2>
+ <div
+ className="_content"
+ >
+ <div />
+ </div>
+ </li>
+</ul>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap
new file mode 100644
index 0000000000..cbeeeaa3f2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Badge render 1`] = `
+<span
+ className="badge text-white text-center"
+>
+ 3
+</span>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap
new file mode 100644
index 0000000000..5078cebc9e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BracketArrow render 1`] = `
+<div
+ className="bracket-arrow down"
+ style={
+ Object {
+ "bottom": 50,
+ "left": 10,
+ "top": 20,
+ }
+ }
+/>
+`;
+
+exports[`BracketArrow render up 1`] = `
+<div
+ className="bracket-arrow up"
+ style={
+ Object {
+ "bottom": 50,
+ "left": 10,
+ "top": 20,
+ }
+ }
+/>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap
new file mode 100644
index 0000000000..fd60784327
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dropdown render 1`] = `
+<div
+ className="dropdown-block"
+>
+ <div
+ className="dropdown"
+ onClick={[Function]}
+ style={
+ Object {
+ "display": "block",
+ }
+ }
+ >
+ <div />
+ </div>
+ <button
+ className="dropdown-button"
+ onClick={[Function]}
+ >
+ ✅
+ </button>
+ <div
+ className="dropdown-mask"
+ onClick={[Function]}
+ style={
+ Object {
+ "display": "block",
+ }
+ }
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap
new file mode 100644
index 0000000000..e9b9639749
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Modal renders 1`] = `
+<div
+ className="modal-wrapper"
+ onClick={[Function]}
+>
+ <div
+ className="modal entering"
+ onClick={[Function]}
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap
new file mode 100644
index 0000000000..1c3589a6f8
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap
@@ -0,0 +1,549 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popover mount popover 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="popover"
+>
+ <div
+ className="popover orientation-right"
+ style={
+ Object {
+ "left": 500,
+ "top": -50,
+ }
+ }
+ >
+ <BracketArrow
+ left={-4}
+ orientation="left"
+ top={98}
+ >
+ <div
+ className="bracket-arrow left"
+ style={
+ Object {
+ "bottom": undefined,
+ "left": -4,
+ "top": 98,
+ }
+ }
+ />
+ </BracketArrow>
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": 500,
+ "orientation": "right",
+ "targetMid": Object {
+ "x": -14,
+ "y": 98,
+ },
+ "top": -50,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="popover orientation-right"
+ style="top: -50px; left: 500px;"
+ >
+ <div
+ class="bracket-arrow left"
+ style="left: -4px; top: 98px;"
+ />
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,300,100,0,100,0,0,400,100,400,100,300"
+ />
+ </svg>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="popover"
+ >
+ <svg
+ style={
+ Object {
+ "height": 0,
+ "marginLeft": -100,
+ "marginTop": undefined,
+ "position": "absolute",
+ "width": 480,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ 300,
+ 100,
+ 0,
+ 100,
+ 0,
+ 0,
+ 400,
+ 100,
+ 400,
+ 100,
+ 300,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover mount tooltip 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="tooltip"
+>
+ <div
+ className="tooltip orientation-down"
+ style={
+ Object {
+ "left": -8,
+ "top": 0,
+ }
+ }
+ >
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": -8,
+ "orientation": "down",
+ "targetMid": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "top": 0,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="tooltip orientation-down"
+ style="top: 0px; left: -8px;"
+ >
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,-250,0,-250,28,100,128,100"
+ />
+ </svg>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="tooltip"
+ >
+ <svg
+ style={
+ Object {
+ "height": -250,
+ "marginLeft": undefined,
+ "marginTop": -100,
+ "position": "absolute",
+ "width": 100,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ -250,
+ 0,
+ -250,
+ 28,
+ 100,
+ 128,
+ 100,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover render (tooltip) 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="tooltip"
+>
+ <div
+ className="tooltip orientation-down"
+ style={
+ Object {
+ "left": -8,
+ "top": 0,
+ }
+ }
+ >
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": -8,
+ "orientation": "down",
+ "targetMid": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "top": 0,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="tooltip orientation-down"
+ style="top: 0px; left: -8px;"
+ >
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,-250,0,-250,28,100,128,100"
+ />
+ </svg>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="tooltip"
+ >
+ <svg
+ style={
+ Object {
+ "height": -250,
+ "marginLeft": undefined,
+ "marginTop": -100,
+ "position": "absolute",
+ "width": 100,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ -250,
+ 0,
+ -250,
+ 28,
+ 100,
+ 128,
+ 100,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover render 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="popover"
+>
+ <div
+ className="popover orientation-right"
+ style={
+ Object {
+ "left": 500,
+ "top": -50,
+ }
+ }
+ >
+ <BracketArrow
+ left={-4}
+ orientation="left"
+ top={98}
+ >
+ <div
+ className="bracket-arrow left"
+ style={
+ Object {
+ "bottom": undefined,
+ "left": -4,
+ "top": 98,
+ }
+ }
+ />
+ </BracketArrow>
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": 500,
+ "orientation": "right",
+ "targetMid": Object {
+ "x": -14,
+ "y": 98,
+ },
+ "top": -50,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="popover orientation-right"
+ style="top: -50px; left: 500px;"
+ >
+ <div
+ class="bracket-arrow left"
+ style="left: -4px; top: 98px;"
+ />
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,300,100,0,100,0,0,400,100,400,100,300"
+ />
+ </svg>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="popover"
+ >
+ <svg
+ style={
+ Object {
+ "height": 0,
+ "marginLeft": -100,
+ "marginTop": undefined,
+ "position": "absolute",
+ "width": 480,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ 300,
+ 100,
+ 0,
+ 100,
+ 0,
+ 0,
+ 400,
+ 100,
+ 400,
+ 100,
+ 300,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+</Popover>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap
new file mode 100644
index 0000000000..e766bd45aa
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PreviewFunction should return a span 1`] = `
+<span
+ className="function-signature"
+>
+ <span
+ className="function-name"
+ >
+ &lt;anonymous&gt;
+ </span>
+ <span
+ className="paren"
+ >
+ (
+ </span>
+ <span
+ className="paren"
+ >
+ )
+ </span>
+</span>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap
new file mode 100644
index 0000000000..d3d8b27575
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Result list should render the component 1`] = `
+<ul
+ aria-live="polite"
+ className="result-list small"
+ id="result-list"
+ role="listbox"
+>
+ <li
+ aria-describedby="0-subtitle"
+ aria-labelledby="0-title"
+ className="result-item"
+ key="0value0"
+ onClick={[Function]}
+ role="option"
+ title="value"
+ >
+ <div
+ className="title"
+ id="0-title"
+ >
+ title
+ </div>
+ <div
+ className="subtitle"
+ id="0-subtitle"
+ >
+ subtitle
+ </div>
+ </li>
+ <li
+ aria-describedby="1-subtitle"
+ aria-labelledby="1-title"
+ className="result-item selected"
+ key="1value 11"
+ onClick={[Function]}
+ role="option"
+ title="value 1"
+ >
+ <div
+ className="title"
+ id="1-title"
+ >
+ title 1
+ </div>
+ <div
+ className="subtitle"
+ id="1-subtitle"
+ >
+ subtitle 1
+ </div>
+ </li>
+</ul>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap
new file mode 100644
index 0000000000..c56a13dc3b
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap
@@ -0,0 +1,267 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchInput renders 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows nav buttons 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows svg error emoji 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows svg magnifying glass 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;