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