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.css195
-rw-r--r--devtools/client/debugger/src/components/shared/AccessibleImage.js20
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.css73
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.js84
-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.js31
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CloseButton.js30
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CommandBarButton.js55
-rw-r--r--devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js58
-rw-r--r--devtools/client/debugger/src/components/shared/Button/index.js11
-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.css54
-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.js26
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js40
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js53
-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.snap10
-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.js76
-rw-r--r--devtools/client/debugger/src/components/shared/ManagedTree.css43
-rw-r--r--devtools/client/debugger/src/components/shared/ManagedTree.js162
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.css51
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.js76
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.css32
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.js336
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.css23
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.js93
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.css139
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.js85
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.css132
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.js272
-rw-r--r--devtools/client/debugger/src/components/shared/SmartGap.js169
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.css150
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.js65
-rw-r--r--devtools/client/debugger/src/components/shared/menu.css56
-rw-r--r--devtools/client/debugger/src/components/shared/moz.build24
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Accordion.spec.js42
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Badge.spec.js14
-rw-r--r--devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js21
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js18
-rw-r--r--devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js117
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Modal.spec.js52
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Popover.spec.js202
-rw-r--r--devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js155
-rw-r--r--devtools/client/debugger/src/components/shared/tests/ResultList.spec.js51
-rw-r--r--devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js116
-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__/ManagedTree.spec.js.snap543
-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.snap235
62 files changed, 5391 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..48e4d8e13a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.css
@@ -0,0 +1,195 @@
+/* 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.case-match {
+ mask-image: url(chrome://devtools/content/debugger/images/case-match.svg);
+}
+
+.img.close {
+ mask-image: url(chrome://devtools/content/debugger/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/content/debugger/images/info.svg);
+}
+
+.img.loader {
+ mask-image: url(chrome://devtools/content/debugger/images/loader.svg);
+}
+
+.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/content/debugger/images/plus.svg);
+}
+
+.img.prettyPrint {
+ mask-image: url(chrome://devtools/content/debugger/images/prettyPrint.svg);
+}
+
+.img.refresh {
+ mask-image: url(chrome://devtools/content/debugger/images/reload.svg);
+}
+
+.img.regex-match {
+ mask-image: url(chrome://devtools/content/debugger/images/regex-match.svg);
+}
+
+.img.resume {
+ mask-image: url(chrome://devtools/content/debugger/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/debugger/images/stepOver.svg);
+}
+
+.img.tab {
+ mask-image: url(chrome://devtools/content/debugger/images/tab.svg);
+}
+
+.img.whole-word-match {
+ mask-image: url(chrome://devtools/content/debugger/images/whole-word-match.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..662944df3e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.js
@@ -0,0 +1,20 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import classnames from "classnames";
+
+import "./AccessibleImage.css";
+
+const AccessibleImage = (props: Object) => {
+ props = {
+ ...props,
+ className: classnames("img", props.className),
+ };
+ return <span {...props} />;
+};
+
+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..6751c30199
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Accordion.js
@@ -0,0 +1,84 @@
+/* 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/>. */
+
+// @flow
+import React, { cloneElement, Component } from "react";
+import AccessibleImage from "./AccessibleImage";
+
+import "./Accordion.css";
+
+type AccordionItem = {
+ buttons?: Array<Object>,
+ component: React$Element<any>,
+ componentProps: Object,
+ header: string,
+ className: string,
+ opened: boolean,
+ onToggle?: () => void,
+ shouldOpen?: () => void,
+};
+
+type Props = { items: Array<Object> };
+
+class Accordion extends Component<Props> {
+ handleHeaderClick(i: number) {
+ 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: SyntheticKeyboardEvent<HTMLHeadingElement>,
+ i: number
+ ) {
+ if (e && (e.key === " " || e.key === "Enter")) {
+ this.handleHeaderClick(i);
+ }
+ }
+
+ renderContainer = (item: AccordionItem, i: number) => {
+ 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..7f0c4fc216
--- /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/>. */
+
+// @flow
+import React from "react";
+import "./Badge.css";
+
+type Props = {
+ children: number,
+};
+
+const Badge = ({ children }: Props) => (
+ <span className="badge text-white text-center">{children}</span>
+);
+
+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..9cb036cb92
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/BracketArrow.js
@@ -0,0 +1,31 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import classNames from "classnames";
+
+import "./BracketArrow.css";
+
+const BracketArrow = ({
+ orientation,
+ left,
+ top,
+ bottom,
+}: {
+ orientation: string,
+ left: number,
+ top: number,
+ bottom: number,
+}) => {
+ return (
+ <div
+ className={classNames("bracket-arrow", orientation || "up")}
+ style={{ left, top, bottom }}
+ />
+ );
+};
+
+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..4075e901bf
--- /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/>. */
+
+// @flow
+import React from "react";
+
+import AccessibleImage from "../AccessibleImage";
+
+import "./styles/CloseButton.css";
+
+type Props = {
+ handleClick: Function,
+ buttonClass?: string,
+ tooltip?: string,
+};
+
+function CloseButton({ handleClick, buttonClass, tooltip }: Props) {
+ return (
+ <button
+ className={buttonClass ? `close-btn ${buttonClass}` : "close-btn"}
+ onClick={handleClick}
+ title={tooltip}
+ >
+ <AccessibleImage className="close" />
+ </button>
+ );
+}
+
+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..0226bc0765
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js
@@ -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/>. */
+
+// @flow
+import classnames from "classnames";
+import React from "react";
+
+import AccessibleImage from "../AccessibleImage";
+
+import "./styles/CommandBarButton.css";
+
+type Props = {
+ children: React$Element<any>,
+ className: string,
+ pressed?: boolean,
+};
+
+export function debugBtn(
+ onClick: ?Function,
+ type: string,
+ className: string,
+ tooltip: string,
+ disabled: boolean = false,
+ ariaPressed: boolean = false
+) {
+ return (
+ <CommandBarButton
+ className={classnames(type, className)}
+ disabled={disabled}
+ key={type}
+ onClick={onClick}
+ pressed={ariaPressed}
+ title={tooltip}
+ >
+ <AccessibleImage className={type} />
+ </CommandBarButton>
+ );
+}
+
+const CommandBarButton = (props: Props) => {
+ const { children, className, pressed = false, ...rest } = props;
+
+ return (
+ <button
+ aria-pressed={pressed}
+ className={classnames("command-bar-button", className)}
+ {...rest}
+ >
+ {children}
+ </button>
+ );
+};
+
+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..16e129317b
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js
@@ -0,0 +1,58 @@
+/* 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/>. */
+
+// @flow
+import React, { PureComponent } from "react";
+import classnames from "classnames";
+import AccessibleImage from "../AccessibleImage";
+import { CommandBarButton } from "./";
+import "./styles/PaneToggleButton.css";
+
+type Position = "start" | "end";
+
+type Props = {
+ collapsed: boolean,
+ handleClick: (Position, boolean) => void,
+ horizontal: boolean,
+ position: Position,
+};
+
+class PaneToggleButton extends PureComponent<Props> {
+ static defaultProps = {
+ horizontal: false,
+ position: "start",
+ };
+
+ label(position: Position, collapsed: boolean) {
+ switch (position) {
+ case "start":
+ return L10N.getStr(collapsed ? "expandSources" : "collapseSources");
+ case "end":
+ return L10N.getStr(
+ collapsed ? "expandBreakpoints" : "collapseBreakpoints"
+ );
+ }
+ }
+
+ 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..d97b27e6a1
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/index.js
@@ -0,0 +1,11 @@
+/* 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/>. */
+
+// @flow
+
+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..0267b41365
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
@@ -0,0 +1,54 @@
+/* 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");
+}
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..b7953ac598
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js
@@ -0,0 +1,26 @@
+/* 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/>. */
+
+// @flow
+
+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..fdb5691d8d
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.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/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+import { CommandBarButton, debugBtn } from "../";
+
+describe("CommandBarButton", () => {
+ it("renders", () => {
+ const wrapper = shallow(
+ <CommandBarButton children={([]: any)} className={""} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders children", () => {
+ const children = [1, 2, 3, 4];
+ const wrapper = shallow(
+ <CommandBarButton children={(children: any)} 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 onClick={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..7c9f8e4c0a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js
@@ -0,0 +1,53 @@
+/* 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/>. */
+
+// @flow
+
+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..c4c6ce895a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CommandBarButton renders 1`] = `
+<button
+ aria-pressed={false}
+ className="command-bar-button"
+/>
+`;
+
+exports[`debugBtn renders 1`] = `<debugBtn />`;
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..6bac9cc3c3
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Dropdown.js
@@ -0,0 +1,76 @@
+/* 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/>. */
+
+// @flow
+import React, { Component } from "react";
+import type { Node } from "react";
+import "./Dropdown.css";
+
+type Props = {
+ panel: React$Element<any>,
+ icon: Node,
+};
+
+type State = {
+ dropdownShown: boolean,
+};
+
+export class Dropdown extends Component<Props, State> {
+ toggleDropdown: Function;
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ dropdownShown: false,
+ };
+ }
+
+ toggleDropdown = (e: SyntheticKeyboardEvent<HTMLElement>) => {
+ 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 (
+ // eslint-disable-next-line prettier/prettier
+ <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/ManagedTree.css b/devtools/client/debugger/src/components/shared/ManagedTree.css
new file mode 100644
index 0000000000..9c8bf05d5a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ManagedTree.css
@@ -0,0 +1,43 @@
+/* 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/>. */
+
+.managed-tree .tree {
+ user-select: none;
+
+ white-space: nowrap;
+ overflow: auto;
+ min-width: 100%;
+
+ display: grid;
+ grid-template-columns: 1fr;
+ align-content: start;
+
+ line-height: 1.4em;
+}
+
+.managed-tree .tree button {
+ display: block;
+}
+
+.managed-tree .tree .node {
+ padding: 2px 3px 2px 3px;
+ position: relative;
+}
+
+.managed-tree .tree .node.focused {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+html:not([dir="rtl"]) .managed-tree .tree .node > div {
+ margin-left: 10px;
+}
+
+html[dir="rtl"] .managed-tree .tree .node > div {
+ margin-right: 10px;
+}
+
+.managed-tree .tree-node button {
+ position: fixed;
+}
diff --git a/devtools/client/debugger/src/components/shared/ManagedTree.js b/devtools/client/debugger/src/components/shared/ManagedTree.js
new file mode 100644
index 0000000000..6574782cd0
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ManagedTree.js
@@ -0,0 +1,162 @@
+/* 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/>. */
+
+// @flow
+import React, { Component } from "react";
+import "./ManagedTree.css";
+
+// $FlowIgnore
+const Tree = require("devtools/client/shared/components/Tree");
+
+export type Item = {
+ contents: any,
+ name: string,
+ path: string,
+};
+
+type Props = {
+ autoExpandAll: boolean,
+ autoExpandDepth: number,
+ getChildren: Object => Object[],
+ getPath: (Object, index?: number) => string,
+ getParent: Item => any,
+ getRoots: () => any,
+ highlightItems?: Array<Item>,
+ itemHeight: number,
+ listItems?: Array<Item>,
+ onFocus: (item: any) => void,
+ onExpand?: (item: Item, expanded: Set<string>) => void,
+ onCollapse?: (item: Item, expanded: Set<string>) => void,
+ renderItem: any,
+ focused?: any,
+ expanded?: any,
+};
+
+type State = {
+ expanded: any,
+};
+
+class ManagedTree extends Component<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ expanded: props.expanded || new Set(),
+ };
+ }
+
+ static defaultProps = {
+ onFocus: () => {},
+ };
+
+ componentWillReceiveProps(nextProps: Props) {
+ const { listItems, highlightItems } = this.props;
+ if (nextProps.listItems && nextProps.listItems != listItems) {
+ this.expandListItems(nextProps.listItems);
+ }
+
+ if (
+ nextProps.highlightItems &&
+ nextProps.highlightItems != highlightItems &&
+ nextProps.highlightItems.length
+ ) {
+ this.highlightItem(nextProps.highlightItems);
+ }
+ }
+
+ setExpanded = (
+ item: Item,
+ isExpanded: boolean,
+ shouldIncludeChildren: boolean
+ ) => {
+ const expandItem = i => {
+ const path = this.props.getPath(i);
+ if (isExpanded) {
+ expanded.add(path);
+ } else {
+ expanded.delete(path);
+ }
+ };
+ const { expanded } = this.state;
+ expandItem(item);
+
+ if (shouldIncludeChildren) {
+ let parents = [item];
+ while (parents.length) {
+ const children = [];
+ for (const parent of parents) {
+ if (parent.contents?.length) {
+ for (const child of parent.contents) {
+ expandItem(child);
+ children.push(child);
+ }
+ }
+ }
+ parents = children;
+ }
+ }
+ this.setState({ expanded });
+
+ if (isExpanded && this.props.onExpand) {
+ this.props.onExpand(item, expanded);
+ } else if (!isExpanded && this.props.onCollapse) {
+ this.props.onCollapse(item, expanded);
+ }
+ };
+
+ expandListItems(listItems: Array<Item>) {
+ const { expanded } = this.state;
+ listItems.forEach(item => expanded.add(this.props.getPath(item)));
+ this.props.onFocus(listItems[0]);
+ this.setState({ expanded });
+ }
+
+ highlightItem(highlightItems: Array<Item>) {
+ const { expanded } = this.state;
+ // This file is visible, so we highlight it.
+ if (expanded.has(this.props.getPath(highlightItems[0]))) {
+ this.props.onFocus(highlightItems[0]);
+ } else {
+ // Look at folders starting from the top-level until finds a
+ // closed folder and highlights this folder
+ const index = highlightItems
+ .reverse()
+ .findIndex(
+ item =>
+ !expanded.has(this.props.getPath(item)) && item.name !== "root"
+ );
+
+ if (highlightItems[index]) {
+ this.props.onFocus(highlightItems[index]);
+ }
+ }
+ }
+
+ render() {
+ const { expanded } = this.state;
+ return (
+ <div className="managed-tree">
+ <Tree
+ {...this.props}
+ isExpanded={item => expanded.has(this.props.getPath(item))}
+ focused={this.props.focused}
+ getKey={this.props.getPath}
+ onExpand={(item, shouldIncludeChildren) =>
+ this.setExpanded(item, true, shouldIncludeChildren)
+ }
+ onCollapse={(item, shouldIncludeChildren) =>
+ this.setExpanded(item, false, shouldIncludeChildren)
+ }
+ onFocus={this.props.onFocus}
+ renderItem={(...args) =>
+ this.props.renderItem(...args, {
+ setExpanded: this.setExpanded,
+ })
+ }
+ />
+ </div>
+ );
+ }
+}
+
+export default ManagedTree;
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..88e0ad6645
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Modal.js
@@ -0,0 +1,76 @@
+/* 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/>. */
+
+// @flow
+
+import PropTypes from "prop-types";
+import React from "react";
+import type { Node as ReactNode } from "react";
+import classnames from "classnames";
+import Transition from "react-transition-group/Transition";
+import "./Modal.css";
+
+type TransitionStatus = "entering" | "exiting" | "entered" | "exited";
+
+type ModalProps = {
+ status: TransitionStatus,
+ children?: ReactNode,
+ additionalClass?: string,
+ handleClose: () => any,
+};
+
+export const transitionTimeout = 50;
+
+export class Modal extends React.Component<ModalProps> {
+ onClick = (e: SyntheticEvent<HTMLElement>) => {
+ 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,
+};
+
+type SlideProps = {
+ in: boolean,
+ children?: ReactNode,
+ additionalClass?: string,
+ handleClose: () => any,
+};
+
+export default function Slide({
+ in: inProp,
+ children,
+ additionalClass,
+ handleClose,
+}: SlideProps) {
+ return (
+ <Transition in={inProp} timeout={transitionTimeout} appear>
+ {(status: TransitionStatus) => (
+ <Modal
+ status={status}
+ additionalClass={additionalClass}
+ handleClose={handleClose}
+ >
+ {children}
+ </Modal>
+ )}
+ </Transition>
+ );
+}
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..5b5b78d364
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.js
@@ -0,0 +1,336 @@
+/* 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/>. */
+
+// @flow
+import React, { Component } from "react";
+import classNames from "classnames";
+import BracketArrow from "./BracketArrow";
+import SmartGap from "./SmartGap";
+
+import "./Popover.css";
+
+type Props = {
+ editorRef: ?HTMLDivElement,
+ targetPosition: Object,
+ children: ?React$Element<any>,
+ target: HTMLDivElement,
+ type?: "popover" | "tooltip",
+ mouseout: Function,
+};
+
+type Orientation = "up" | "down" | "right";
+type TargetMid = {
+ x: number,
+ y: number,
+};
+export type Coords = {
+ left: number,
+ top: number,
+ targetMid: TargetMid,
+ orientation: Orientation,
+};
+
+type State = { coords: Coords };
+
+class Popover extends Component<Props, State> {
+ $popover: ?HTMLDivElement;
+ $tooltip: ?HTMLDivElement;
+ $gap: ?HTMLDivElement;
+ timerId: ?TimeoutID;
+ wasOnGap: boolean;
+ state = {
+ coords: {
+ left: 0,
+ top: 0,
+ orientation: "down",
+ targetMid: { x: 0, y: 0 },
+ },
+ };
+ firstRender = true;
+ gapHeight: number;
+ gapHeight: number;
+
+ static defaultProps = {
+ type: "popover",
+ };
+
+ componentDidMount() {
+ const { type } = this.props;
+ // $FlowIgnore
+ 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;
+ }
+ return this.props.mouseout();
+ }
+
+ // 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: ClientRect,
+ editor: ClientRect,
+ popover: ClientRect,
+ orientation?: 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: ClientRect,
+ editor: ClientRect,
+ popover: ClientRect
+ ) => {
+ 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: ClientRect,
+ editor: ClientRect,
+ popover: ClientRect
+ ) {
+ 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: ClientRect,
+ editor: ClientRect,
+ popover: ClientRect,
+ orientation?: string
+ ) => {
+ 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}
+ // $FlowIgnore
+ offset={this.$gap.getBoundingClientRect().left}
+ />
+ </div>
+ );
+ }
+
+ getPopoverArrow(orientation: Orientation, left: number, top: number) {
+ 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={classNames("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..b02a410f01
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/PreviewFunction.js
@@ -0,0 +1,93 @@
+/* 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/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { times, zip, flatten } from "lodash";
+
+import { formatDisplayName } from "../../utils/pause/frames";
+
+import type { URL } from "../../types";
+
+import "./PreviewFunction.css";
+
+type FunctionType = {
+ name?: string,
+ displayName?: string,
+ userDisplayName?: string,
+ parameterNames?: string[],
+ location?: {
+ url: URL,
+ line: number,
+ column: number,
+ },
+};
+
+type Props = { func: FunctionType };
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+export default class PreviewFunction extends Component<Props> {
+ renderFunctionName(func: FunctionType) {
+ const { l10n } = this.context;
+ const name = formatDisplayName((func: any), undefined, l10n);
+ return <span className="function-name">{name}</span>;
+ }
+
+ renderParams(func: FunctionType) {
+ const { parameterNames = [] } = func;
+ const params = parameterNames.filter(Boolean).map(param => (
+ <span className="param" key={param}>
+ {param}
+ </span>
+ ));
+
+ const commas = times(params.length - 1).map((_, i) => (
+ <span className="delimiter" key={i}>
+ {", "}
+ </span>
+ ));
+
+ // $FlowIgnore
+ return flatten(zip(params, commas));
+ }
+
+ jumpToDefinitionButton(func: FunctionType) {
+ const { location } = func;
+
+ if (
+ location &&
+ location.url &&
+ !IGNORED_SOURCE_URLS.includes(location.url)
+ ) {
+ 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..f3c586932c
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.css
@@ -0,0 +1,139 @@
+/* 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);
+}
+
+.result-list li.selected {
+ background: var(--theme-accordion-header-background);
+}
+
+.result-list.small li.selected {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.theme-dark .result-list.small li.selected {
+ background-color: var(--theme-body-background);
+}
+
+.theme-dark .result-list li:hover {
+ background: var(--grey-70);
+}
+
+.theme-dark .result-list li.selected {
+ background: var(--grey-70);
+}
+
+.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..1dfc511931
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.js
@@ -0,0 +1,85 @@
+/* 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/>. */
+
+// @flow
+import React, { Component } from "react";
+import classnames from "classnames";
+
+import AccessibleImage from "./AccessibleImage";
+
+import "./ResultList.css";
+
+type Props = {
+ items: Array<any>,
+ selected: number,
+ selectItem: (
+ event: SyntheticKeyboardEvent<HTMLElement>,
+ item: any,
+ index: number
+ ) => void,
+ size: string,
+ role: string,
+};
+
+export default class ResultList extends Component<Props> {
+ displayName: "ResultList";
+
+ static defaultProps = {
+ size: "small",
+ role: "listbox",
+ };
+
+ renderListItem = (item: any, index: number) => {
+ 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..e6d6d3a683
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.css
@@ -0,0 +1,132 @@
+/* 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;
+}
+
+.search-outline:focus-within {
+ border-color: var(--blue-50);
+}
+
+.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;
+ padding: 4px;
+ padding-inline-start: 28px;
+ line-height: 16px;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--theme-body-color);
+ background-color: transparent;
+}
+
+.search-field.big input {
+ height: 40px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-inline-start: 40px;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.search-field input:focus {
+ outline: none;
+}
+
+.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;
+}
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..896f9caf9e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.js
@@ -0,0 +1,272 @@
+/* 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/>. */
+
+// @flow
+
+import React, { Component } from "react";
+
+import { CloseButton } from "./Button";
+
+import AccessibleImage from "./AccessibleImage";
+import classnames from "classnames";
+import "./SearchInput.css";
+
+const arrowBtn = (onClick, type, className, tooltip) => {
+ const props = {
+ className,
+ key: type,
+ onClick,
+ title: tooltip,
+ type,
+ };
+
+ return (
+ <button {...props}>
+ <AccessibleImage className={type} />
+ </button>
+ );
+};
+
+type Props = {
+ count: number,
+ expanded: boolean,
+ handleClose?: (e: SyntheticMouseEvent<HTMLDivElement>) => void,
+ handleNext?: (e: SyntheticMouseEvent<HTMLButtonElement>) => void,
+ handlePrev?: (e: SyntheticMouseEvent<HTMLButtonElement>) => void,
+ hasPrefix?: boolean,
+ onBlur?: (e: SyntheticFocusEvent<HTMLInputElement>) => void,
+ onChange: (e: SyntheticInputEvent<HTMLInputElement>) => void,
+ onFocus?: (e: SyntheticFocusEvent<HTMLInputElement>) => void,
+ onKeyDown: (e: SyntheticKeyboardEvent<HTMLInputElement>) => void,
+ onKeyUp?: (e: SyntheticKeyboardEvent<HTMLInputElement>) => void,
+ onHistoryScroll?: (historyValue: string) => void,
+ placeholder: string,
+ query: string,
+ selectedItemId?: string,
+ shouldFocus?: boolean,
+ showErrorEmoji: boolean,
+ size: string,
+ summaryMsg: string,
+ showClose: boolean,
+ isLoading: boolean,
+};
+
+type State = {
+ history: Array<string>,
+};
+
+class SearchInput extends Component<Props, State> {
+ displayName: "SearchInput";
+ $input: ?HTMLInputElement;
+
+ static defaultProps = {
+ expanded: false,
+ hasPrefix: false,
+ selectedItemId: "",
+ size: "",
+ showClose: true,
+ };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ history: [],
+ };
+ }
+
+ componentDidMount() {
+ this.setFocus();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ 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);
+ }
+ }
+
+ renderSvg() {
+ return <AccessibleImage className="search" />;
+ }
+
+ 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: SyntheticFocusEvent<HTMLInputElement>) => {
+ const { onFocus } = this.props;
+
+ if (onFocus) {
+ onFocus(e);
+ }
+ };
+
+ onBlur = (e: SyntheticFocusEvent<HTMLInputElement>) => {
+ const { onBlur } = this.props;
+
+ if (onBlur) {
+ onBlur(e);
+ }
+ };
+
+ onKeyDown = (e: any) => {
+ const { onHistoryScroll, onKeyDown } = this.props;
+ if (!onHistoryScroll) {
+ return onKeyDown(e);
+ }
+
+ const inputValue = e.target.value;
+ const { history } = this.state;
+ const currentHistoryIndex = history.indexOf(inputValue);
+
+ if (e.key === "Enter") {
+ this.saveEnteredTerm(inputValue);
+ return onKeyDown(e);
+ }
+
+ 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);
+ }
+ }
+ };
+
+ saveEnteredTerm(query: string) {
+ 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 <AccessibleImage className="loader spin" />;
+ }
+ }
+
+ renderNav() {
+ const { count, handleNext, handlePrev } = this.props;
+ if ((!handleNext && !handlePrev) || !count || count == 1) {
+ return;
+ }
+
+ return (
+ <div className="search-nav-buttons">{this.renderArrowButtons()}</div>
+ );
+ }
+
+ render() {
+ const {
+ expanded,
+ handleClose,
+ onChange,
+ onKeyUp,
+ placeholder,
+ query,
+ selectedItemId,
+ showErrorEmoji,
+ size,
+ showClose,
+ } = 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}
+ >
+ {this.renderSvg()}
+ <input {...inputProps} />
+ {this.renderSpinner()}
+ {this.renderSummaryMsg()}
+ {this.renderNav()}
+ {showClose && (
+ <CloseButton handleClick={handleClose} buttonClass={size} />
+ )}
+ </div>
+ </div>
+ );
+ }
+}
+
+export default 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..ce26c0d35e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SmartGap.js
@@ -0,0 +1,169 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import type { Coords } from "./Popover";
+
+type Props = {
+ token: HTMLDivElement,
+ preview: ?HTMLDivElement,
+ type: ?string,
+ gapHeight: number,
+ coords: Coords,
+ offset: number,
+};
+
+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: ClientRect,
+ token: ClientRect,
+ offset: number,
+ orientation: string,
+ gapHeight: number,
+ coords: 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: ClientRect,
+ tokenRect: ClientRect,
+ offset: number,
+ orientation: string,
+ gapHeight: number,
+ coords: 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,
+}: Props) {
+ const tokenRect = token.getBoundingClientRect();
+ // $FlowIgnore
+ 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>
+ );
+}
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..e7a9f53ff6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.css
@@ -0,0 +1,150 @@
+/* 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 {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/coffeescript.svg);
+}
+
+.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 {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/javascript.svg);
+ mask-size: 14px 14px;
+}
+
+.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 {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/react.svg);
+ background-color: var(--theme-highlight-bluegrey);
+}
+
+.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 {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/typescript.svg);
+}
+
+.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..25e88ea69d
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.js
@@ -0,0 +1,65 @@
+/* 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/>. */
+
+// @flow
+
+import React, { PureComponent } from "react";
+
+import { connect } from "../../utils/connect";
+
+import AccessibleImage from "./AccessibleImage";
+
+import { getSourceClassnames, isPretty } from "../../utils/source";
+import { getFramework } from "../../utils/tabs";
+import { getSymbols, getTabs } from "../../selectors";
+
+import type { Source } from "../../types";
+import type { Symbols } from "../../reducers/types";
+
+import "./SourceIcon.css";
+
+type OwnProps = {|
+ source: Source,
+
+ // An additional validator for the icon returned
+ modifier?: string => string | null,
+|};
+type Props = {
+ source: Source,
+ modifier?: string => string | null,
+
+ // symbols will provide framework information
+ symbols: ?Symbols,
+ framework: ?string,
+};
+
+class SourceIcon extends PureComponent<Props> {
+ render() {
+ const { modifier, source, symbols, framework } = this.props;
+ let iconClass = "";
+
+ if (isPretty(source)) {
+ iconClass = "prettyPrint";
+ } else {
+ iconClass = framework
+ ? framework.toLowerCase()
+ : getSourceClassnames(source, symbols);
+ }
+
+ if (modifier) {
+ const modified = modifier(iconClass);
+ if (!modified) {
+ return null;
+ }
+ iconClass = modified;
+ }
+
+ return <AccessibleImage className={`source-icon ${iconClass}`} />;
+ }
+}
+
+export default connect<Props, OwnProps, _, _, _, _>((state, props) => ({
+ symbols: getSymbols(state, props.source),
+ framework: getFramework(getTabs(state), props.source.url),
+}))(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..579cdf5dd9
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/menu.css
@@ -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/>. */
+
+menupopup {
+ position: fixed;
+ z-index: 10000;
+ background: white;
+ 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..97fc3e5465
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/moz.build
@@ -0,0 +1,24 @@
+# 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",
+ "ManagedTree.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..6a8ce2fb6e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js
@@ -0,0 +1,42 @@
+/* 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/>. */
+
+// @flow
+
+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..ea13b89c83
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js
@@ -0,0 +1,14 @@
+/* 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/>. */
+
+// @flow
+
+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..b66f60f5f1
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js
@@ -0,0 +1,21 @@
+/* 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/>. */
+
+// @flow
+
+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..66cfd5f91a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js
@@ -0,0 +1,18 @@
+/* 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/>. */
+
+// @flow
+
+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/ManagedTree.spec.js b/devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js
new file mode 100644
index 0000000000..f647a46a65
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js
@@ -0,0 +1,117 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import { mount, shallow } from "enzyme";
+
+import ManagedTree from "../ManagedTree";
+
+function getTestContent() {
+ const testTree = {
+ a: {
+ value: "FOO",
+ children: [
+ { value: 1 },
+ { value: 2 },
+ { value: 3 },
+ { value: 4 },
+ { value: 5 },
+ ],
+ },
+ b: {
+ value: "BAR",
+ children: [
+ { value: "A" },
+ { value: "B" },
+ { value: "C" },
+ { value: "D" },
+ { value: "E" },
+ ],
+ },
+ c: { value: "BAZ" },
+ };
+ const renderItem = item => <div>{item.value ? item.value : item}</div>;
+ const onFocus = jest.fn();
+ const onExpand = jest.fn();
+ const onCollapse = jest.fn();
+ const getPath = (item, i) => {
+ if (item.value) {
+ return item.value;
+ }
+ if (i) {
+ return `${i}`;
+ }
+ return `${item}-$`;
+ };
+
+ return {
+ testTree,
+ props: {
+ getRoots: () => Object.keys(testTree),
+ getParent: item => null,
+ getChildren: branch => branch.children || [],
+ itemHeight: 24,
+ autoExpandAll: true,
+ autoExpandDepth: 1,
+ getPath,
+ renderItem,
+ onFocus,
+ onExpand,
+ onCollapse,
+ },
+ };
+}
+
+describe("ManagedTree", () => {
+ it("render", () =>
+ expect(
+ shallow(<ManagedTree {...getTestContent().props} />)
+ ).toMatchSnapshot());
+ it("expands list items", () => {
+ const { props, testTree } = getTestContent();
+ const wrapper = shallow(<ManagedTree {...props} />);
+ wrapper.setProps({
+ listItems: testTree.b.children,
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+ it("highlights list items", () => {
+ const { props, testTree } = getTestContent();
+ const wrapper = shallow(<ManagedTree {...props} />);
+ wrapper.setProps({
+ highlightItems: testTree.a.children,
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("sets expanded items", () => {
+ const { props, testTree } = getTestContent();
+ const wrapper = mount(<ManagedTree {...props} />);
+ expect(wrapper).toMatchSnapshot();
+ // We auto-expanded the first layer, so unexpand first node.
+ wrapper
+ .find("TreeNode")
+ .first()
+ .simulate("click");
+ expect(wrapper).toMatchSnapshot();
+ expect(props.onExpand).toHaveBeenCalledWith(
+ "c",
+ new Set(
+ Object.keys(testTree)
+ .filter(i => i !== "a")
+ .map(k => `${k}-$`)
+ )
+ );
+ wrapper
+ .find("TreeNode")
+ .first()
+ .simulate("click");
+ expect(props.onExpand).toHaveBeenCalledWith(
+ "c",
+ new Set(Object.keys(testTree).map(k => `${k}-$`))
+ );
+ });
+});
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..163f0bc654
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js
@@ -0,0 +1,52 @@
+/* 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/>. */
+
+// @flow
+
+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: any = "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..8b3f312722
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js
@@ -0,0 +1,202 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import { mount } from "enzyme";
+
+import Popover from "../Popover";
+
+describe("Popover", () => {
+ const onMouseLeave = jest.fn();
+ const onKeyDown = jest.fn();
+ const editorRef: any = {
+ getBoundingClientRect() {
+ return {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 250,
+ right: 0,
+ bottom: 0,
+ left: 20,
+ };
+ },
+ };
+
+ const targetRef: any = {
+ 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: any = {
+ 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: any = {
+ 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..06897683c0
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js
@@ -0,0 +1,155 @@
+/* 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/>. */
+
+// @flow
+
+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..242e4a7aec
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/ResultList.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/>. */
+
+// @flow
+
+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..357de84c4f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js
@@ -0,0 +1,116 @@
+/* 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/>. */
+
+// @flow
+
+import React from "react";
+import { shallow } from "enzyme";
+
+import SearchInput from "../SearchInput";
+
+describe("SearchInput", () => {
+ // !! wrapper is defined outside test scope
+ // so it will keep values between tests
+ const wrapper = shallow(
+ <SearchInput
+ query=""
+ count={5}
+ placeholder="A placeholder"
+ summaryMsg="So many results"
+ showErrorEmoji={false}
+ isLoading={false}
+ onChange={() => {}}
+ onKeyDown={() => {}}
+ />
+ );
+
+ 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__/ManagedTree.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ManagedTree.spec.js.snap
new file mode 100644
index 0000000000..ef785a6398
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ManagedTree.spec.js.snap
@@ -0,0 +1,543 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManagedTree expands list items 1`] = `
+<div
+ className="managed-tree"
+>
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ isExpanded={[Function]}
+ itemHeight={24}
+ listItems={
+ Array [
+ Object {
+ "value": "A",
+ },
+ Object {
+ "value": "B",
+ },
+ Object {
+ "value": "C",
+ },
+ Object {
+ "value": "D",
+ },
+ Object {
+ "value": "E",
+ },
+ ]
+ }
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "value": "A",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ renderItem={[Function]}
+ />
+</div>
+`;
+
+exports[`ManagedTree highlights list items 1`] = `
+<div
+ className="managed-tree"
+>
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ highlightItems={
+ Array [
+ Object {
+ "value": 5,
+ },
+ Object {
+ "value": 4,
+ },
+ Object {
+ "value": 3,
+ },
+ Object {
+ "value": 2,
+ },
+ Object {
+ "value": 1,
+ },
+ ]
+ }
+ isExpanded={[Function]}
+ itemHeight={24}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "value": 5,
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ renderItem={[Function]}
+ />
+</div>
+`;
+
+exports[`ManagedTree render 1`] = `
+<div
+ className="managed-tree"
+>
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ isExpanded={[Function]}
+ itemHeight={24}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[MockFunction]}
+ renderItem={[Function]}
+ />
+</div>
+`;
+
+exports[`ManagedTree sets expanded items 1`] = `
+<ManagedTree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ itemHeight={24}
+ onCollapse={[MockFunction]}
+ onExpand={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ Set {
+ "a-$",
+ "b-$",
+ "c-$",
+ },
+ ],
+ Array [
+ "b",
+ Set {
+ "a-$",
+ "b-$",
+ "c-$",
+ },
+ ],
+ Array [
+ "c",
+ Set {
+ "a-$",
+ "b-$",
+ "c-$",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ onFocus={[MockFunction]}
+ renderItem={[Function]}
+>
+ <div
+ className="managed-tree"
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ isExpanded={[Function]}
+ itemHeight={24}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={[MockFunction]}
+ renderItem={[Function]}
+ >
+ <div
+ className="tree "
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={false}
+ id="a-$"
+ index={0}
+ isExpandable={false}
+ item="a"
+ key="a-$-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="a-$"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ a
+ </div>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={false}
+ id="1"
+ index={1}
+ isExpandable={false}
+ item="b"
+ key="1-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="1"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ b
+ </div>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={false}
+ id="2"
+ index={2}
+ isExpandable={false}
+ item="c"
+ key="2-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="2"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ c
+ </div>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </div>
+</ManagedTree>
+`;
+
+exports[`ManagedTree sets expanded items 2`] = `
+<ManagedTree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ itemHeight={24}
+ onCollapse={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ Set {
+ "b-$",
+ "c-$",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ onExpand={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ Set {
+ "b-$",
+ "c-$",
+ },
+ ],
+ Array [
+ "b",
+ Set {
+ "b-$",
+ "c-$",
+ },
+ ],
+ Array [
+ "c",
+ Set {
+ "b-$",
+ "c-$",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ onFocus={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ renderItem={[Function]}
+>
+ <div
+ className="managed-tree"
+ >
+ <Tree
+ autoExpandAll={true}
+ autoExpandDepth={1}
+ getChildren={[Function]}
+ getKey={[Function]}
+ getParent={[Function]}
+ getPath={[Function]}
+ getRoots={[Function]}
+ isExpanded={[Function]}
+ itemHeight={24}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ onFocus={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ renderItem={[Function]}
+ >
+ <div
+ className="tree "
+ onBlur={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ onKeyPress={[Function]}
+ onKeyUp={[Function]}
+ role="tree"
+ style={Object {}}
+ tabIndex="0"
+ >
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={false}
+ focused={false}
+ id="a-$"
+ index={0}
+ isExpandable={false}
+ item="a"
+ key="a-$-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="a-$"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ a
+ </div>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={false}
+ id="1"
+ index={1}
+ isExpandable={false}
+ item="b"
+ key="1-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="1"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ b
+ </div>
+ </div>
+ </TreeNode>
+ <TreeNode
+ active={false}
+ depth={0}
+ expanded={true}
+ focused={false}
+ id="2"
+ index={2}
+ isExpandable={false}
+ item="c"
+ key="2-inactive"
+ onClick={[Function]}
+ onCollapse={[Function]}
+ onExpand={[Function]}
+ renderItem={[Function]}
+ >
+ <div
+ aria-expanded={true}
+ aria-level={1}
+ className="tree-node"
+ data-expandable={false}
+ id="2"
+ onClick={[Function]}
+ onKeyDownCapture={null}
+ role="treeitem"
+ >
+ <div>
+ c
+ </div>
+ </div>
+ </TreeNode>
+ </div>
+ </Tree>
+ </div>
+</ManagedTree>
+`;
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..50fd748f47
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap
@@ -0,0 +1,235 @@
+// 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>
+ <CloseButton
+ buttonClass=""
+ />
+ </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>
+ <CloseButton
+ buttonClass=""
+ />
+ </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>
+ <CloseButton
+ buttonClass=""
+ />
+ </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>
+ <CloseButton
+ buttonClass=""
+ />
+ </div>
+</div>
+`;