diff options
Diffstat (limited to 'devtools/client/debugger/src/components/shared')
62 files changed, 5391 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.css b/devtools/client/debugger/src/components/shared/AccessibleImage.css new file mode 100644 index 0000000000..48e4d8e13a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/AccessibleImage.css @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.img { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + /* use background-color for the icon color, and mask-image for its shape */ + background-color: var(--theme-icon-color); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + /* multicolor icons use background-image */ + background-position: center; + background-repeat: no-repeat; + background-size: contain; + /* do not let images shrink when used as flex children */ + flex-shrink: 0; +} + +/* Expand arrow icon */ +.img.arrow { + width: 10px; + height: 10px; + mask-image: url(chrome://devtools/content/debugger/images/arrow.svg); + /* we may override the width/height in specific contexts to make the + clickable area bigger, but we should always keep the mask size 10x10 */ + mask-size: 10px 10px; + background-color: var(--theme-icon-dimmed-color); + transform: rotate(-90deg); + transition: transform 180ms var(--animation-curve); +} + +.img.arrow:dir(rtl) { + transform: rotate(90deg); +} + +.img.arrow.expanded { + /* icon should always point to the bottom (default) when expanded, + regardless of the text direction */ + transform: none !important; +} + +.img.arrow-down { + mask-image: url(chrome://devtools/content/debugger/images/arrow-down.svg); +} + +.img.arrow-up { + mask-image: url(chrome://devtools/content/debugger/images/arrow-up.svg); +} + +.img.blackBox { + mask-image: url(chrome://devtools/content/debugger/images/blackBox.svg); +} + +.img.breadcrumb { + mask-image: url(chrome://devtools/content/debugger/images/breadcrumbs-divider.svg); +} + +.img.case-match { + mask-image: url(chrome://devtools/content/debugger/images/case-match.svg); +} + +.img.close { + mask-image: url(chrome://devtools/content/debugger/images/close.svg); +} + +.img.disable-pausing { + mask-image: url(chrome://devtools/content/debugger/images/disable-pausing.svg); +} + +.img.enable-pausing { + mask-image: url(chrome://devtools/content/debugger/images/enable-pausing.svg); + background-color: var(--theme-icon-checked-color); +} + +.img.globe { + mask-image: url(chrome://devtools/content/debugger/images/globe.svg); +} + +.img.globe-small { + mask-image: url(chrome://devtools/content/debugger/images/globe-small.svg); + mask-size: 12px 12px; +} + +.img.window { + mask-image: url(chrome://devtools/content/debugger/images/window.svg); +} + +.img.file { + mask-image: url(chrome://devtools/content/debugger/images/file-small.svg); + mask-size: 12px 12px; +} + +.img.folder { + mask-image: url(chrome://devtools/content/debugger/images/folder.svg); +} + +.img.home { + mask-image: url(chrome://devtools/content/debugger/images/home.svg); +} + +.img.info { + mask-image: url(chrome://devtools/content/debugger/images/info.svg); +} + +.img.loader { + mask-image: url(chrome://devtools/content/debugger/images/loader.svg); +} + +.img.more-tabs { + mask-image: url(chrome://devtools/content/debugger/images/command-chevron.svg); +} + +html[dir="rtl"] .img.more-tabs { + transform: scaleX(-1); +} + +.img.next { + mask-image: url(chrome://devtools/content/debugger/images/next.svg); +} + +.img.next-circle { + mask-image: url(chrome://devtools/content/debugger/images/next-circle.svg); +} + +.img.pane-collapse { + mask-image: url(chrome://devtools/content/debugger/images/pane-collapse.svg); +} + +.img.pane-expand { + mask-image: url(chrome://devtools/content/debugger/images/pane-expand.svg); +} + +.img.pause { + mask-image: url(chrome://devtools/content/debugger/images/pause.svg); +} + +.img.plus { + mask-image: url(chrome://devtools/content/debugger/images/plus.svg); +} + +.img.prettyPrint { + mask-image: url(chrome://devtools/content/debugger/images/prettyPrint.svg); +} + +.img.refresh { + mask-image: url(chrome://devtools/content/debugger/images/reload.svg); +} + +.img.regex-match { + mask-image: url(chrome://devtools/content/debugger/images/regex-match.svg); +} + +.img.resume { + mask-image: url(chrome://devtools/content/debugger/images/resume.svg); +} + +.img.search { + mask-image: url(chrome://devtools/content/debugger/images/search.svg); +} + +.img.shortcuts { + mask-image: url(chrome://devtools/content/debugger/images/help.svg); +} + +.img.spin { + animation: spin 0.5s linear infinite; +} + +.img.stepIn { + mask-image: url(chrome://devtools/content/debugger/images/stepIn.svg); +} + +.img.stepOut { + mask-image: url(chrome://devtools/content/debugger/images/stepOut.svg); +} + +.img.stepOver { + mask-image: url(chrome://devtools/content/debugger/images/stepOver.svg); +} + +.img.tab { + mask-image: url(chrome://devtools/content/debugger/images/tab.svg); +} + +.img.whole-word-match { + mask-image: url(chrome://devtools/content/debugger/images/whole-word-match.svg); +} + +.img.worker { + mask-image: url(chrome://devtools/content/debugger/images/worker.svg); +} diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.js b/devtools/client/debugger/src/components/shared/AccessibleImage.js new file mode 100644 index 0000000000..662944df3e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/AccessibleImage.js @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import classnames from "classnames"; + +import "./AccessibleImage.css"; + +const AccessibleImage = (props: Object) => { + props = { + ...props, + className: classnames("img", props.className), + }; + return <span {...props} />; +}; + +export default AccessibleImage; diff --git a/devtools/client/debugger/src/components/shared/Accordion.css b/devtools/client/debugger/src/components/shared/Accordion.css new file mode 100644 index 0000000000..e87fa41a6f --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Accordion.css @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.accordion { + background-color: var(--theme-sidebar-background); + width: 100%; + list-style-type: none; + padding: 0px; + margin-top: 0px; +} + +.accordion ._header { + background-color: var(--theme-accordion-header-background); + border-bottom: 1px solid var(--theme-splitter-color); + display: flex; + font-size: 12px; + line-height: calc(16 / 12); + padding: 4px 6px; + width: 100%; + align-items: center; + margin: 0px; + font-weight: normal; + cursor: default; + user-select: none; +} + +.accordion ._header:hover { + background-color: var(--theme-accordion-header-hover); +} + +.accordion ._header .arrow { + margin-inline-end: 4px; +} + +.accordion ._header .header-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--theme-toolbar-color); +} + +.accordion ._header .header-buttons { + display: flex; + margin-inline-start: auto; +} + +.accordion ._header .header-buttons button { + color: var(--theme-body-color); + border: none; + background: none; + padding: 0; + margin: 0 2px; + width: 16px; + height: 16px; +} + +.accordion ._header .header-buttons button::-moz-focus-inner { + border: none; +} + +.accordion ._header .header-buttons button .img { + display: block; +} + +.accordion ._content { + border-bottom: 1px solid var(--theme-splitter-color); + font-size: var(--theme-body-font-size); +} + +.accordion div:last-child ._content { + border-bottom: none; +} diff --git a/devtools/client/debugger/src/components/shared/Accordion.js b/devtools/client/debugger/src/components/shared/Accordion.js new file mode 100644 index 0000000000..6751c30199 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Accordion.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { cloneElement, Component } from "react"; +import AccessibleImage from "./AccessibleImage"; + +import "./Accordion.css"; + +type AccordionItem = { + buttons?: Array<Object>, + component: React$Element<any>, + componentProps: Object, + header: string, + className: string, + opened: boolean, + onToggle?: () => void, + shouldOpen?: () => void, +}; + +type Props = { items: Array<Object> }; + +class Accordion extends Component<Props> { + handleHeaderClick(i: number) { + const item = this.props.items[i]; + const opened = !item.opened; + item.opened = opened; + + if (item.onToggle) { + item.onToggle(opened); + } + + // We force an update because otherwise the accordion + // would not re-render + this.forceUpdate(); + } + + onHandleHeaderKeyDown( + e: SyntheticKeyboardEvent<HTMLHeadingElement>, + i: number + ) { + if (e && (e.key === " " || e.key === "Enter")) { + this.handleHeaderClick(i); + } + } + + renderContainer = (item: AccordionItem, i: number) => { + const { opened } = item; + + return ( + <li className={item.className} key={i}> + <h2 + className="_header" + tabIndex="0" + onKeyDown={e => this.onHandleHeaderKeyDown(e, i)} + onClick={() => this.handleHeaderClick(i)} + > + <AccessibleImage className={`arrow ${opened ? "expanded" : ""}`} /> + <span className="header-label">{item.header}</span> + {item.buttons ? ( + <div className="header-buttons" tabIndex="-1"> + {item.buttons} + </div> + ) : null} + </h2> + {opened && ( + <div className="_content"> + {cloneElement(item.component, item.componentProps || {})} + </div> + )} + </li> + ); + }; + render() { + return ( + <ul className="accordion"> + {this.props.items.map(this.renderContainer)} + </ul> + ); + } +} + +export default Accordion; diff --git a/devtools/client/debugger/src/components/shared/Badge.css b/devtools/client/debugger/src/components/shared/Badge.css new file mode 100644 index 0000000000..f52d32edf4 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Badge.css @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.badge { + --size: 17px; + --radius: calc(var(--size) / 2); + height: var(--size); + min-width: var(--size); + line-height: var(--size); + background: var(--theme-toolbar-background-hover); + color: var(--theme-body-color); + border-radius: var(--radius); + padding: 0 4px; + font-size: 0.9em; +} diff --git a/devtools/client/debugger/src/components/shared/Badge.js b/devtools/client/debugger/src/components/shared/Badge.js new file mode 100644 index 0000000000..7f0c4fc216 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Badge.js @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React from "react"; +import "./Badge.css"; + +type Props = { + children: number, +}; + +const Badge = ({ children }: Props) => ( + <span className="badge text-white text-center">{children}</span> +); + +export default Badge; diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.css b/devtools/client/debugger/src/components/shared/BracketArrow.css new file mode 100644 index 0000000000..afca888371 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/BracketArrow.css @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.bracket-arrow { + position: absolute; + pointer-events: none; +} + +.bracket-arrow::before, +.bracket-arrow::after { + content: ""; + height: 0; + width: 0; + position: absolute; + border: 7px solid transparent; +} + +.bracket-arrow.up::before { + border-bottom-color: var(--theme-splitter-color); + top: -1px; +} + +.theme-dark .bracket-arrow.up::before { + border-bottom-color: var(--theme-body-color); +} + +.bracket-arrow.up::after { + border-bottom-color: var(--theme-body-background); + top: 0px; +} + +.bracket-arrow.down::before { + border-bottom-color: transparent; + border-top-color: var(--theme-splitter-color); + top: 0px; +} + +.theme-dark .bracket-arrow.down::before { + border-top-color: var(--theme-body-color); +} + +.bracket-arrow.down::after { + border-bottom-color: transparent; + border-top-color: var(--theme-body-background); + top: -1px; +} + +.bracket-arrow.left::before { + border-left-color: transparent; + border-right-color: var(--theme-splitter-color); + top: 0px; +} + +.theme-dark .bracket-arrow.left::before { + border-right-color: var(--theme-body-color); +} + +.bracket-arrow.left::after { + border-left-color: transparent; + border-right-color: var(--theme-body-background); + top: 0px; + left: 1px; +} diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.js b/devtools/client/debugger/src/components/shared/BracketArrow.js new file mode 100644 index 0000000000..9cb036cb92 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/BracketArrow.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import classNames from "classnames"; + +import "./BracketArrow.css"; + +const BracketArrow = ({ + orientation, + left, + top, + bottom, +}: { + orientation: string, + left: number, + top: number, + bottom: number, +}) => { + return ( + <div + className={classNames("bracket-arrow", orientation || "up")} + style={{ left, top, bottom }} + /> + ); +}; + +export default BracketArrow; diff --git a/devtools/client/debugger/src/components/shared/Button/CloseButton.js b/devtools/client/debugger/src/components/shared/Button/CloseButton.js new file mode 100644 index 0000000000..4075e901bf --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/CloseButton.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React from "react"; + +import AccessibleImage from "../AccessibleImage"; + +import "./styles/CloseButton.css"; + +type Props = { + handleClick: Function, + buttonClass?: string, + tooltip?: string, +}; + +function CloseButton({ handleClick, buttonClass, tooltip }: Props) { + return ( + <button + className={buttonClass ? `close-btn ${buttonClass}` : "close-btn"} + onClick={handleClick} + title={tooltip} + > + <AccessibleImage className="close" /> + </button> + ); +} + +export default CloseButton; diff --git a/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js new file mode 100644 index 0000000000..0226bc0765 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import classnames from "classnames"; +import React from "react"; + +import AccessibleImage from "../AccessibleImage"; + +import "./styles/CommandBarButton.css"; + +type Props = { + children: React$Element<any>, + className: string, + pressed?: boolean, +}; + +export function debugBtn( + onClick: ?Function, + type: string, + className: string, + tooltip: string, + disabled: boolean = false, + ariaPressed: boolean = false +) { + return ( + <CommandBarButton + className={classnames(type, className)} + disabled={disabled} + key={type} + onClick={onClick} + pressed={ariaPressed} + title={tooltip} + > + <AccessibleImage className={type} /> + </CommandBarButton> + ); +} + +const CommandBarButton = (props: Props) => { + const { children, className, pressed = false, ...rest } = props; + + return ( + <button + aria-pressed={pressed} + className={classnames("command-bar-button", className)} + {...rest} + > + {children} + </button> + ); +}; + +export default CommandBarButton; diff --git a/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js new file mode 100644 index 0000000000..16e129317b --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { PureComponent } from "react"; +import classnames from "classnames"; +import AccessibleImage from "../AccessibleImage"; +import { CommandBarButton } from "./"; +import "./styles/PaneToggleButton.css"; + +type Position = "start" | "end"; + +type Props = { + collapsed: boolean, + handleClick: (Position, boolean) => void, + horizontal: boolean, + position: Position, +}; + +class PaneToggleButton extends PureComponent<Props> { + static defaultProps = { + horizontal: false, + position: "start", + }; + + label(position: Position, collapsed: boolean) { + switch (position) { + case "start": + return L10N.getStr(collapsed ? "expandSources" : "collapseSources"); + case "end": + return L10N.getStr( + collapsed ? "expandBreakpoints" : "collapseBreakpoints" + ); + } + } + + render() { + const { position, collapsed, horizontal, handleClick } = this.props; + + return ( + <CommandBarButton + className={classnames("toggle-button", position, { + collapsed, + vertical: !horizontal, + })} + onClick={() => handleClick(position, !collapsed)} + title={this.label(position, collapsed)} + > + <AccessibleImage + className={collapsed ? "pane-expand" : "pane-collapse"} + /> + </CommandBarButton> + ); + } +} + +export default PaneToggleButton; diff --git a/devtools/client/debugger/src/components/shared/Button/index.js b/devtools/client/debugger/src/components/shared/Button/index.js new file mode 100644 index 0000000000..d97b27e6a1 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/index.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import CloseButton from "./CloseButton"; +import CommandBarButton, { debugBtn } from "./CommandBarButton"; +import PaneToggleButton from "./PaneToggleButton"; + +export { CloseButton, CommandBarButton, debugBtn, PaneToggleButton }; diff --git a/devtools/client/debugger/src/components/shared/Button/moz.build b/devtools/client/debugger/src/components/shared/Button/moz.build new file mode 100644 index 0000000000..c6e652d5dc --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/moz.build @@ -0,0 +1,15 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "styles", +] + +CompiledModules( + "CloseButton.js", + "CommandBarButton.js", + "index.js", + "PaneToggleButton.js", +) diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css new file mode 100644 index 0000000000..b0093ff4de --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.close-btn { + width: 16px; + height: 16px; + border: 1px solid transparent; + border-radius: 2px; + padding: 1px; + color: var(--theme-icon-color); +} + +.close-btn:hover, +.close-btn:focus { + color: var(--theme-selection-color); + background-color: var(--theme-selection-background); +} + +.close-btn .img { + display: block; + width: 12px; + height: 12px; + /* inherit the button's text color for the icon's color */ + background-color: currentColor; +} + +.close-btn.big { + width: 20px; + height: 20px; +} + +.close-btn.big .img { + width: 16px; + height: 16px; +} diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css new file mode 100644 index 0000000000..0267b41365 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.command-bar-button { + appearance: none; + background: transparent; + border: none; + display: inline-block; + text-align: center; + position: relative; + padding: 0px 5px; + fill: currentColor; + min-width: 30px; +} + +.command-bar-button:disabled { + opacity: 0.6; + cursor: default; +} + +.command-bar-button:not(.disabled):hover, +.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover { + background: var(--theme-toolbar-background-hover); +} + +.theme-dark .command-bar-button:not(.disabled):hover, +.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover { + background: var(--theme-toolbar-hover); +} + +:root.theme-dark .command-bar-button { + color: var(--theme-body-color); +} + +.command-bar-button > * { + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; +} + +/** + * Settings icon and menu + */ +.devtools-button.debugger-settings-menu-button { + border-radius: 0; + margin: 0; + padding: 0; +} + +.devtools-button.debugger-settings-menu-button::before { + background-image: url("chrome://devtools/skin/images/settings.svg"); +} diff --git a/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css new file mode 100644 index 0000000000..d8a2495408 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.toggle-button { + padding: 4px 6px; +} + +.toggle-button .img { + vertical-align: middle; +} + +.toggle-button.end { + margin-inline-end: 0px; + margin-inline-start: auto; +} + +.toggle-button.start { + margin-inline-start: 0px; +} + +html[dir="rtl"] .toggle-button.start .img, +html[dir="ltr"] .toggle-button.end:not(.vertical) .img { + transform: scaleX(-1); +} + +.toggle-button.end.vertical .img { + transform: rotate(-90deg); +} diff --git a/devtools/client/debugger/src/components/shared/Button/styles/moz.build b/devtools/client/debugger/src/components/shared/Button/styles/moz.build new file mode 100644 index 0000000000..7d80140dbe --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [] + +CompiledModules() diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js new file mode 100644 index 0000000000..b7953ac598 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; +import { CloseButton } from "../"; + +describe("CloseButton", () => { + it("renders with tooltip", () => { + const tooltip = "testTooltip"; + const wrapper = shallow( + <CloseButton tooltip={tooltip} handleClick={() => {}} /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles click event", () => { + const handleClickSpy = jest.fn(); + const wrapper = shallow(<CloseButton handleClick={handleClickSpy} />); + wrapper.simulate("click"); + expect(handleClickSpy).toHaveBeenCalled(); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js new file mode 100644 index 0000000000..fdb5691d8d --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; +import { CommandBarButton, debugBtn } from "../"; + +describe("CommandBarButton", () => { + it("renders", () => { + const wrapper = shallow( + <CommandBarButton children={([]: any)} className={""} /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("renders children", () => { + const children = [1, 2, 3, 4]; + const wrapper = shallow( + <CommandBarButton children={(children: any)} className={""} /> + ); + expect(wrapper.find("button").children()).toHaveLength(4); + }); +}); + +describe("debugBtn", () => { + it("renders", () => { + const wrapper = shallow(<debugBtn />); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles onClick", () => { + const onClickSpy = jest.fn(); + const wrapper = shallow(<debugBtn onClick={onClickSpy} />); + wrapper.simulate("click"); + expect(onClickSpy).toHaveBeenCalled(); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js new file mode 100644 index 0000000000..7c9f8e4c0a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; +import { PaneToggleButton } from "../"; + +describe("PaneToggleButton", () => { + const handleClickSpy = jest.fn(); + const wrapper = shallow( + <PaneToggleButton + handleClick={handleClickSpy} + collapsed={false} + position="start" + /> + ); + + it("renders default", () => { + expect(wrapper.hasClass("vertical")).toBe(true); + expect(wrapper).toMatchSnapshot(); + }); + + it("toggles horizontal class", () => { + wrapper.setProps({ horizontal: true }); + expect(wrapper.hasClass("vertical")).toBe(false); + }); + + it("toggles collapsed class", () => { + wrapper.setProps({ collapsed: true }); + expect(wrapper.hasClass("collapsed")).toBe(true); + }); + + it("toggles start position", () => { + wrapper.setProps({ position: "start" }); + expect(wrapper.hasClass("start")).toBe(true); + }); + + it("toggles end position ", () => { + wrapper.setProps({ position: "end" }); + expect(wrapper.hasClass("end")).toBe(true); + }); + + it("handleClick is called", () => { + const position = "end"; + const collapsed = false; + wrapper.setProps({ position, collapsed }); + wrapper.simulate("click"); + expect(handleClickSpy).toHaveBeenCalledWith(position, true); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap new file mode 100644 index 0000000000..d0a0cb9967 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CloseButton renders with tooltip 1`] = ` +<button + className="close-btn" + onClick={[Function]} + title="testTooltip" +> + <AccessibleImage + className="close" + /> +</button> +`; diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap new file mode 100644 index 0000000000..c4c6ce895a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandBarButton renders 1`] = ` +<button + aria-pressed={false} + className="command-bar-button" +/> +`; + +exports[`debugBtn renders 1`] = `<debugBtn />`; diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap new file mode 100644 index 0000000000..86067066a6 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaneToggleButton renders default 1`] = ` +<CommandBarButton + className="toggle-button start vertical" + onClick={[Function]} + title="Collapse Sources and Outline panes" +> + <AccessibleImage + className="pane-collapse" + /> +</CommandBarButton> +`; diff --git a/devtools/client/debugger/src/components/shared/Dropdown.css b/devtools/client/debugger/src/components/shared/Dropdown.css new file mode 100644 index 0000000000..bae5656c8f --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Dropdown.css @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.dropdown { + background: var(--theme-body-background); + border: 1px solid var(--theme-splitter-color); + border-radius: 4px; + box-shadow: 0 4px 4px 0 var(--search-overlays-semitransparent); + max-height: 300px; + position: absolute; + top: 24px; + width: 150px; + z-index: 1000; + overflow: auto; +} + +[dir="ltr"] .dropdown { + right: 2px; +} + +[dir="rtl"] .dropdown { + left: 2px; +} + +.dropdown-block { + position: relative; + align-self: center; + height: 100%; +} + +/* cover the reserved space at the end of .source-tabs */ +.source-tabs + .dropdown-block { + margin-inline-start: -28px; +} + +.dropdown-button { + color: var(--theme-comment); + background: none; + border: none; + padding: 4px 6px; + font-weight: 100; + font-size: 14px; + height: 100%; + width: 28px; +} + +.dropdown-button .img { + display: block; +} + +.dropdown ul { + margin: 0; + padding: 4px 0; + list-style: none; +} + +.dropdown li { + display: flex; + align-items: center; + padding: 6px 8px; + font-size: 12px; + line-height: calc(16 / 12); + transition: all 0.25s ease; +} + +.dropdown li:hover { + background-color: var(--search-overlays-semitransparent); +} + +.dropdown-icon { + margin-inline-end: 4px; + mask-size: 13px 13px; +} + +.dropdown-label { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown-icon.prettyPrint, +.dropdown-icon.blackBox { + background-color: var(--theme-highlight-blue); +} + +.dropdown-mask { + position: fixed; + width: 100%; + height: 100%; + background: transparent; + z-index: 999; + left: 0; + top: 0; +} diff --git a/devtools/client/debugger/src/components/shared/Dropdown.js b/devtools/client/debugger/src/components/shared/Dropdown.js new file mode 100644 index 0000000000..6bac9cc3c3 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Dropdown.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { Component } from "react"; +import type { Node } from "react"; +import "./Dropdown.css"; + +type Props = { + panel: React$Element<any>, + icon: Node, +}; + +type State = { + dropdownShown: boolean, +}; + +export class Dropdown extends Component<Props, State> { + toggleDropdown: Function; + constructor(props: Props) { + super(props); + this.state = { + dropdownShown: false, + }; + } + + toggleDropdown = (e: SyntheticKeyboardEvent<HTMLElement>) => { + this.setState(prevState => ({ + dropdownShown: !prevState.dropdownShown, + })); + }; + + renderPanel() { + return ( + <div + className="dropdown" + onClick={this.toggleDropdown} + style={{ display: this.state.dropdownShown ? "block" : "none" }} + > + {this.props.panel} + </div> + ); + } + + renderButton() { + return ( + // eslint-disable-next-line prettier/prettier + <button className="dropdown-button" onClick={this.toggleDropdown}> + {this.props.icon} + </button> + ); + } + + renderMask() { + return ( + <div + className="dropdown-mask" + onClick={this.toggleDropdown} + style={{ display: this.state.dropdownShown ? "block" : "none" }} + /> + ); + } + + render() { + return ( + <div className="dropdown-block"> + {this.renderPanel()} + {this.renderButton()} + {this.renderMask()} + </div> + ); + } +} + +export default Dropdown; diff --git a/devtools/client/debugger/src/components/shared/ManagedTree.css b/devtools/client/debugger/src/components/shared/ManagedTree.css new file mode 100644 index 0000000000..9c8bf05d5a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ManagedTree.css @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.managed-tree .tree { + user-select: none; + + white-space: nowrap; + overflow: auto; + min-width: 100%; + + display: grid; + grid-template-columns: 1fr; + align-content: start; + + line-height: 1.4em; +} + +.managed-tree .tree button { + display: block; +} + +.managed-tree .tree .node { + padding: 2px 3px 2px 3px; + position: relative; +} + +.managed-tree .tree .node.focused { + color: var(--theme-selection-color); + background-color: var(--theme-selection-background); +} + +html:not([dir="rtl"]) .managed-tree .tree .node > div { + margin-left: 10px; +} + +html[dir="rtl"] .managed-tree .tree .node > div { + margin-right: 10px; +} + +.managed-tree .tree-node button { + position: fixed; +} diff --git a/devtools/client/debugger/src/components/shared/ManagedTree.js b/devtools/client/debugger/src/components/shared/ManagedTree.js new file mode 100644 index 0000000000..6574782cd0 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ManagedTree.js @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { Component } from "react"; +import "./ManagedTree.css"; + +// $FlowIgnore +const Tree = require("devtools/client/shared/components/Tree"); + +export type Item = { + contents: any, + name: string, + path: string, +}; + +type Props = { + autoExpandAll: boolean, + autoExpandDepth: number, + getChildren: Object => Object[], + getPath: (Object, index?: number) => string, + getParent: Item => any, + getRoots: () => any, + highlightItems?: Array<Item>, + itemHeight: number, + listItems?: Array<Item>, + onFocus: (item: any) => void, + onExpand?: (item: Item, expanded: Set<string>) => void, + onCollapse?: (item: Item, expanded: Set<string>) => void, + renderItem: any, + focused?: any, + expanded?: any, +}; + +type State = { + expanded: any, +}; + +class ManagedTree extends Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + expanded: props.expanded || new Set(), + }; + } + + static defaultProps = { + onFocus: () => {}, + }; + + componentWillReceiveProps(nextProps: Props) { + const { listItems, highlightItems } = this.props; + if (nextProps.listItems && nextProps.listItems != listItems) { + this.expandListItems(nextProps.listItems); + } + + if ( + nextProps.highlightItems && + nextProps.highlightItems != highlightItems && + nextProps.highlightItems.length + ) { + this.highlightItem(nextProps.highlightItems); + } + } + + setExpanded = ( + item: Item, + isExpanded: boolean, + shouldIncludeChildren: boolean + ) => { + const expandItem = i => { + const path = this.props.getPath(i); + if (isExpanded) { + expanded.add(path); + } else { + expanded.delete(path); + } + }; + const { expanded } = this.state; + expandItem(item); + + if (shouldIncludeChildren) { + let parents = [item]; + while (parents.length) { + const children = []; + for (const parent of parents) { + if (parent.contents?.length) { + for (const child of parent.contents) { + expandItem(child); + children.push(child); + } + } + } + parents = children; + } + } + this.setState({ expanded }); + + if (isExpanded && this.props.onExpand) { + this.props.onExpand(item, expanded); + } else if (!isExpanded && this.props.onCollapse) { + this.props.onCollapse(item, expanded); + } + }; + + expandListItems(listItems: Array<Item>) { + const { expanded } = this.state; + listItems.forEach(item => expanded.add(this.props.getPath(item))); + this.props.onFocus(listItems[0]); + this.setState({ expanded }); + } + + highlightItem(highlightItems: Array<Item>) { + const { expanded } = this.state; + // This file is visible, so we highlight it. + if (expanded.has(this.props.getPath(highlightItems[0]))) { + this.props.onFocus(highlightItems[0]); + } else { + // Look at folders starting from the top-level until finds a + // closed folder and highlights this folder + const index = highlightItems + .reverse() + .findIndex( + item => + !expanded.has(this.props.getPath(item)) && item.name !== "root" + ); + + if (highlightItems[index]) { + this.props.onFocus(highlightItems[index]); + } + } + } + + render() { + const { expanded } = this.state; + return ( + <div className="managed-tree"> + <Tree + {...this.props} + isExpanded={item => expanded.has(this.props.getPath(item))} + focused={this.props.focused} + getKey={this.props.getPath} + onExpand={(item, shouldIncludeChildren) => + this.setExpanded(item, true, shouldIncludeChildren) + } + onCollapse={(item, shouldIncludeChildren) => + this.setExpanded(item, false, shouldIncludeChildren) + } + onFocus={this.props.onFocus} + renderItem={(...args) => + this.props.renderItem(...args, { + setExpanded: this.setExpanded, + }) + } + /> + </div> + ); + } +} + +export default ManagedTree; diff --git a/devtools/client/debugger/src/components/shared/Modal.css b/devtools/client/debugger/src/components/shared/Modal.css new file mode 100644 index 0000000000..072390b001 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Modal.css @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.modal-wrapper { + position: fixed; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + top: 0; + left: 0; + transition: z-index 200ms; + z-index: 100; +} + +.modal { + display: flex; + width: 80%; + max-height: 80vh; + overflow-y: auto; + background-color: var(--theme-toolbar-background); + transition: transform 150ms cubic-bezier(0.07, 0.95, 0, 1); + box-shadow: 1px 1px 6px 1px var(--popup-shadow-color); +} + +.modal.entering, +.modal.exited { + transform: translateY(-101%); +} + +.modal.entered, +.modal.exiting { + transform: translateY(5px); + flex-direction: column; +} + +/* This rule is active when the screen is not narrow */ +@media (min-width: 580px) { + .modal { + width: 50%; + } +} + +@media (min-height: 340px) { + .modal.entered, + .modal.exiting { + transform: translateY(30px); + } +} diff --git a/devtools/client/debugger/src/components/shared/Modal.js b/devtools/client/debugger/src/components/shared/Modal.js new file mode 100644 index 0000000000..88e0ad6645 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Modal.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import PropTypes from "prop-types"; +import React from "react"; +import type { Node as ReactNode } from "react"; +import classnames from "classnames"; +import Transition from "react-transition-group/Transition"; +import "./Modal.css"; + +type TransitionStatus = "entering" | "exiting" | "entered" | "exited"; + +type ModalProps = { + status: TransitionStatus, + children?: ReactNode, + additionalClass?: string, + handleClose: () => any, +}; + +export const transitionTimeout = 50; + +export class Modal extends React.Component<ModalProps> { + onClick = (e: SyntheticEvent<HTMLElement>) => { + e.stopPropagation(); + }; + + render() { + const { additionalClass, children, handleClose, status } = this.props; + + return ( + <div className="modal-wrapper" onClick={handleClose}> + <div + className={classnames("modal", additionalClass, status)} + onClick={this.onClick} + > + {children} + </div> + </div> + ); + } +} + +Modal.contextTypes = { + shortcuts: PropTypes.object, +}; + +type SlideProps = { + in: boolean, + children?: ReactNode, + additionalClass?: string, + handleClose: () => any, +}; + +export default function Slide({ + in: inProp, + children, + additionalClass, + handleClose, +}: SlideProps) { + return ( + <Transition in={inProp} timeout={transitionTimeout} appear> + {(status: TransitionStatus) => ( + <Modal + status={status} + additionalClass={additionalClass} + handleClose={handleClose} + > + {children} + </Modal> + )} + </Transition> + ); +} diff --git a/devtools/client/debugger/src/components/shared/Popover.css b/devtools/client/debugger/src/components/shared/Popover.css new file mode 100644 index 0000000000..5da8ea4b63 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Popover.css @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.popover { + position: fixed; + z-index: 100; + --gap-size: 10px; + --left-offset: -55px; +} + +.popover.orientation-right { + display: flex; + flex-direction: row; +} + +.popover.orientation-right .gap { + width: var(--gap-size); +} + +.popover:not(.orientation-right) .gap { + height: var(--gap-size); + margin-left: var(--left-offset); +} + +.popover:not(.orientation-right) .preview-popup { + margin-left: var(--left-offset); +} + +.popover .add-to-expression-bar { + margin-left: var(--left-offset); +} diff --git a/devtools/client/debugger/src/components/shared/Popover.js b/devtools/client/debugger/src/components/shared/Popover.js new file mode 100644 index 0000000000..5b5b78d364 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Popover.js @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { Component } from "react"; +import classNames from "classnames"; +import BracketArrow from "./BracketArrow"; +import SmartGap from "./SmartGap"; + +import "./Popover.css"; + +type Props = { + editorRef: ?HTMLDivElement, + targetPosition: Object, + children: ?React$Element<any>, + target: HTMLDivElement, + type?: "popover" | "tooltip", + mouseout: Function, +}; + +type Orientation = "up" | "down" | "right"; +type TargetMid = { + x: number, + y: number, +}; +export type Coords = { + left: number, + top: number, + targetMid: TargetMid, + orientation: Orientation, +}; + +type State = { coords: Coords }; + +class Popover extends Component<Props, State> { + $popover: ?HTMLDivElement; + $tooltip: ?HTMLDivElement; + $gap: ?HTMLDivElement; + timerId: ?TimeoutID; + wasOnGap: boolean; + state = { + coords: { + left: 0, + top: 0, + orientation: "down", + targetMid: { x: 0, y: 0 }, + }, + }; + firstRender = true; + gapHeight: number; + gapHeight: number; + + static defaultProps = { + type: "popover", + }; + + componentDidMount() { + const { type } = this.props; + // $FlowIgnore + this.gapHeight = this.$gap.getBoundingClientRect().height; + const coords = + type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords(); + + if (coords) { + this.setState({ coords }); + } + + this.firstRender = false; + this.startTimer(); + } + + componentWillUnmount() { + if (this.timerId) { + clearTimeout(this.timerId); + } + } + + startTimer() { + this.timerId = setTimeout(this.onTimeout, 0); + } + + onTimeout = () => { + const isHoveredOnGap = this.$gap && this.$gap.matches(":hover"); + const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover"); + const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover"); + const isHoveredOnTarget = this.props.target.matches(":hover"); + + if (isHoveredOnGap) { + if (!this.wasOnGap) { + this.wasOnGap = true; + this.timerId = setTimeout(this.onTimeout, 200); + return; + } + return this.props.mouseout(); + } + + // Don't clear the current preview if mouse is hovered on + // the current preview's token (target) or the popup element + if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) { + this.wasOnGap = false; + this.timerId = setTimeout(this.onTimeout, 0); + return; + } + + this.props.mouseout(); + }; + + calculateLeft( + target: ClientRect, + editor: ClientRect, + popover: ClientRect, + orientation?: Orientation + ) { + const estimatedLeft = target.left; + const estimatedRight = estimatedLeft + popover.width; + const isOverflowingRight = estimatedRight > editor.right; + if (orientation === "right") { + return target.left + target.width; + } + if (isOverflowingRight) { + const adjustedLeft = editor.right - popover.width - 8; + return adjustedLeft; + } + return estimatedLeft; + } + + calculateTopForRightOrientation = ( + target: ClientRect, + editor: ClientRect, + popover: ClientRect + ) => { + if (popover.height <= editor.height) { + const rightOrientationTop = target.top - popover.height / 2; + if (rightOrientationTop < editor.top) { + return editor.top - target.height; + } + const rightOrientationBottom = rightOrientationTop + popover.height; + if (rightOrientationBottom > editor.bottom) { + return editor.bottom + target.height - popover.height + this.gapHeight; + } + return rightOrientationTop; + } + return editor.top - target.height; + }; + + calculateOrientation( + target: ClientRect, + editor: ClientRect, + popover: ClientRect + ) { + const estimatedBottom = target.bottom + popover.height; + if (editor.bottom > estimatedBottom) { + return "down"; + } + const upOrientationTop = target.top - popover.height; + if (upOrientationTop > editor.top) { + return "up"; + } + + return "right"; + } + + calculateTop = ( + target: ClientRect, + editor: ClientRect, + popover: ClientRect, + orientation?: string + ) => { + if (orientation === "down") { + return target.bottom; + } + if (orientation === "up") { + return target.top - popover.height; + } + + return this.calculateTopForRightOrientation(target, editor, popover); + }; + + getPopoverCoords() { + if (!this.$popover || !this.props.editorRef) { + return null; + } + + const popover = this.$popover; + const editor = this.props.editorRef; + const popoverRect = popover.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + const targetRect = this.props.targetPosition; + const orientation = this.calculateOrientation( + targetRect, + editorRect, + popoverRect + ); + const top = this.calculateTop( + targetRect, + editorRect, + popoverRect, + orientation + ); + const popoverLeft = this.calculateLeft( + targetRect, + editorRect, + popoverRect, + orientation + ); + let targetMid; + if (orientation === "right") { + targetMid = { + x: -14, + y: targetRect.top - top - 2, + }; + } else { + targetMid = { + x: targetRect.left - popoverLeft + targetRect.width / 2 - 8, + y: 0, + }; + } + + return { + left: popoverLeft, + top, + orientation, + targetMid, + }; + } + + getTooltipCoords() { + if (!this.$tooltip || !this.props.editorRef) { + return null; + } + const tooltip = this.$tooltip; + const editor = this.props.editorRef; + const tooltipRect = tooltip.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + const targetRect = this.props.targetPosition; + const left = this.calculateLeft(targetRect, editorRect, tooltipRect); + const enoughRoomForTooltipAbove = + targetRect.top - editorRect.top > tooltipRect.height; + const top = enoughRoomForTooltipAbove + ? targetRect.top - tooltipRect.height + : targetRect.bottom; + + return { + left, + top, + orientation: enoughRoomForTooltipAbove ? "up" : "down", + targetMid: { x: 0, y: 0 }, + }; + } + + getChildren() { + const { children } = this.props; + const { coords } = this.state; + const gap = this.getGap(); + + return coords.orientation === "up" ? [children, gap] : [gap, children]; + } + + getGap() { + if (this.firstRender) { + return <div className="gap" key="gap" ref={a => (this.$gap = a)} />; + } + + return ( + <div className="gap" key="gap" ref={a => (this.$gap = a)}> + <SmartGap + token={this.props.target} + preview={this.$tooltip || this.$popover} + type={this.props.type} + gapHeight={this.gapHeight} + coords={this.state.coords} + // $FlowIgnore + offset={this.$gap.getBoundingClientRect().left} + /> + </div> + ); + } + + getPopoverArrow(orientation: Orientation, left: number, top: number) { + let arrowProps = {}; + + if (orientation === "up") { + arrowProps = { orientation: "down", bottom: 10, left }; + } else if (orientation === "down") { + arrowProps = { orientation: "up", top: -2, left }; + } else { + arrowProps = { orientation: "left", top, left: -4 }; + } + + return <BracketArrow {...arrowProps} />; + } + + renderPopover() { + const { top, left, orientation, targetMid } = this.state.coords; + const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y); + + return ( + <div + className={classNames("popover", `orientation-${orientation}`, { + up: orientation === "up", + })} + style={{ top, left }} + ref={c => (this.$popover = c)} + > + {arrow} + {this.getChildren()} + </div> + ); + } + + renderTooltip() { + const { top, left, orientation } = this.state.coords; + return ( + <div + className={classNames("tooltip", `orientation-${orientation}`)} + style={{ top, left }} + ref={c => (this.$tooltip = c)} + > + {this.getChildren()} + </div> + ); + } + + render() { + const { type } = this.props; + + if (type === "tooltip") { + return this.renderTooltip(); + } + + return this.renderPopover(); + } +} + +export default Popover; diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.css b/devtools/client/debugger/src/components/shared/PreviewFunction.css new file mode 100644 index 0000000000..bff9ce25a2 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/PreviewFunction.css @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.function-signature { + align-self: center; +} + +.function-signature .function-name { + color: var(--theme-highlight-blue); +} + +.function-signature .param { + color: var(--theme-highlight-red); +} + +.function-signature .paren { + color: var(--object-color); +} + +.function-signature .comma { + color: var(--object-color); +} diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.js b/devtools/client/debugger/src/components/shared/PreviewFunction.js new file mode 100644 index 0000000000..b02a410f01 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/PreviewFunction.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { times, zip, flatten } from "lodash"; + +import { formatDisplayName } from "../../utils/pause/frames"; + +import type { URL } from "../../types"; + +import "./PreviewFunction.css"; + +type FunctionType = { + name?: string, + displayName?: string, + userDisplayName?: string, + parameterNames?: string[], + location?: { + url: URL, + line: number, + column: number, + }, +}; + +type Props = { func: FunctionType }; + +const IGNORED_SOURCE_URLS = ["debugger eval code"]; + +export default class PreviewFunction extends Component<Props> { + renderFunctionName(func: FunctionType) { + const { l10n } = this.context; + const name = formatDisplayName((func: any), undefined, l10n); + return <span className="function-name">{name}</span>; + } + + renderParams(func: FunctionType) { + const { parameterNames = [] } = func; + const params = parameterNames.filter(Boolean).map(param => ( + <span className="param" key={param}> + {param} + </span> + )); + + const commas = times(params.length - 1).map((_, i) => ( + <span className="delimiter" key={i}> + {", "} + </span> + )); + + // $FlowIgnore + return flatten(zip(params, commas)); + } + + jumpToDefinitionButton(func: FunctionType) { + const { location } = func; + + if ( + location && + location.url && + !IGNORED_SOURCE_URLS.includes(location.url) + ) { + const lastIndex = location.url.lastIndexOf("/"); + + return ( + <button + className="jump-definition" + draggable="false" + title={`${location.url.slice(lastIndex + 1)}:${location.line}`} + /> + ); + } + } + + render() { + const { func } = this.props; + return ( + <span className="function-signature"> + {this.renderFunctionName(func)} + <span className="paren">(</span> + {this.renderParams(func)} + <span className="paren">)</span> + {this.jumpToDefinitionButton(func)} + </span> + ); + } +} + +PreviewFunction.contextTypes = { l10n: PropTypes.object }; diff --git a/devtools/client/debugger/src/components/shared/ResultList.css b/devtools/client/debugger/src/components/shared/ResultList.css new file mode 100644 index 0000000000..f3c586932c --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ResultList.css @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.result-list { + list-style: none; + margin: 0px; + padding: 0px; + overflow: auto; + width: 100%; + background: var(--theme-body-background); +} + +.result-list * { + user-select: none; +} + +.result-list li { + color: var(--theme-body-color); + padding: 4px 8px; + display: flex; +} + +.result-list.big li { + flex-direction: row; + align-items: center; + padding: 6px 8px; + font-size: 12px; + line-height: 16px; +} + +.result-list.small li { + justify-content: space-between; +} + +.result-list li:hover { + background: var(--theme-tab-toolbar-background); +} + +.result-list li.selected { + background: var(--theme-accordion-header-background); +} + +.result-list.small li.selected { + background-color: var(--theme-selection-background); + color: white; +} + +.theme-dark .result-list.small li.selected { + background-color: var(--theme-body-background); +} + +.theme-dark .result-list li:hover { + background: var(--grey-70); +} + +.theme-dark .result-list li.selected { + background: var(--grey-70); +} + +.result-list li .result-item-icon { + background-color: var(--theme-icon-dimmed-color); +} + +.result-list li .icon { + align-self: center; + margin-inline-end: 14px; + margin-inline-start: 4px; +} + +.result-list .result-item-icon { + display: block; +} + +.result-list .selected .result-item-icon { + background-color: var(--theme-selection-color); +} + +.result-list li .title { + word-break: break-all; + text-overflow: ellipsis; + white-space: nowrap; + + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-90); +} + +.theme-dark .result-list li .title { + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-30); +} + +.result-list li.selected .title { + color: white; +} + +.result-list.big li.selected { + background-color: var(--theme-selection-background); + color: white; +} + +.result-list.big li.selected .subtitle { + color: white; +} + +.result-list.big li.selected .subtitle .highlight { + color: white; + font-weight: bold; +} + +.result-list.big li .subtitle { + word-break: break-all; + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-40); + margin-left: 15px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.theme-dark .result-list.big li.selected .subtitle { + color: white; +} + +.theme-dark .result-list.big li .subtitle { + color: var(--theme-text-color-inactive); +} + +.search-bar .result-list li.selected .subtitle { + color: white; +} + +.search-bar .result-list { + border-bottom: 1px solid var(--theme-splitter-color); +} + +.theme-dark .result-list { + background-color: var(--theme-body-background); +} diff --git a/devtools/client/debugger/src/components/shared/ResultList.js b/devtools/client/debugger/src/components/shared/ResultList.js new file mode 100644 index 0000000000..1dfc511931 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ResultList.js @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow +import React, { Component } from "react"; +import classnames from "classnames"; + +import AccessibleImage from "./AccessibleImage"; + +import "./ResultList.css"; + +type Props = { + items: Array<any>, + selected: number, + selectItem: ( + event: SyntheticKeyboardEvent<HTMLElement>, + item: any, + index: number + ) => void, + size: string, + role: string, +}; + +export default class ResultList extends Component<Props> { + displayName: "ResultList"; + + static defaultProps = { + size: "small", + role: "listbox", + }; + + renderListItem = (item: any, index: number) => { + if (item.value === "/" && item.title === "") { + item.title = "(index)"; + } + + const { selectItem, selected } = this.props; + const props = { + onClick: event => selectItem(event, item, index), + key: `${item.id}${item.value}${index}`, + ref: String(index), + title: item.value, + "aria-labelledby": `${item.id}-title`, + "aria-describedby": `${item.id}-subtitle`, + role: "option", + className: classnames("result-item", { + selected: index === selected, + }), + }; + + return ( + <li {...props}> + {item.icon && ( + <div className="icon"> + <AccessibleImage className={item.icon} /> + </div> + )} + <div id={`${item.id}-title`} className="title"> + {item.title} + </div> + {item.subtitle != item.title ? ( + <div id={`${item.id}-subtitle`} className="subtitle"> + {item.subtitle} + </div> + ) : null} + </li> + ); + }; + + render() { + const { size, items, role } = this.props; + + return ( + <ul + className={classnames("result-list", size)} + id="result-list" + role={role} + aria-live="polite" + > + {items.map(this.renderListItem)} + </ul> + ); + } +} diff --git a/devtools/client/debugger/src/components/shared/SearchInput.css b/devtools/client/debugger/src/components/shared/SearchInput.css new file mode 100644 index 0000000000..e6d6d3a683 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SearchInput.css @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +.search-outline { + border: 1px solid var(--theme-toolbar-background); + border-bottom: 1px solid var(--theme-splitter-color); + transition: border-color 200ms ease-in-out; +} + +.search-outline:focus-within { + border-color: var(--blue-50); +} + +.search-field { + position: relative; + display: flex; + align-items: center; + flex-shrink: 0; + min-height: 24px; + width: 100%; + background-color: var(--theme-toolbar-background); +} + +.search-field .img.search { + --icon-mask-size: 12px; + --icon-inset-inline-start: 6px; + position: absolute; + z-index: 1; + top: calc(50% - 8px); + mask-size: var(--icon-mask-size); + background-color: var(--theme-icon-dimmed-color); + pointer-events: none; +} + +.search-field.big .img.search { + --icon-mask-size: 16px; + --icon-inset-inline-start: 12px; +} + +[dir="ltr"] .search-field .img.search { + left: var(--icon-inset-inline-start); +} + +[dir="rtl"] .search-field .img.search { + right: var(--icon-inset-inline-start); +} + +.search-field .img.loader { + width: 24px; + height: 24px; + margin-inline-end: 4px; +} + +.search-field input { + align-self: stretch; + flex-grow: 1; + height: 24px; + width: 40px; + border: none; + padding: 4px; + padding-inline-start: 28px; + line-height: 16px; + font-family: inherit; + font-size: inherit; + color: var(--theme-body-color); + background-color: transparent; +} + +.search-field.big input { + height: 40px; + padding-top: 10px; + padding-bottom: 10px; + padding-inline-start: 40px; + font-size: 14px; + line-height: 20px; +} + +.search-field input:focus { + outline: none; +} + +.search-field input::placeholder { + color: var(--theme-text-color-alt); + opacity: 1; +} + +.search-field-summary { + align-self: center; + padding: 2px 4px; + white-space: nowrap; + text-align: center; + user-select: none; + color: var(--theme-text-color-alt); + /* Avoid layout jumps when we increment the result count quickly. With tabular + numbers, layout will only jump between 9 and 10, 99 and 100, etc. */ + font-variant-numeric: tabular-nums; +} + +.search-field.big .search-field-summary { + margin-inline-end: 4px; +} + +.search-field .search-nav-buttons { + display: flex; + user-select: none; +} + +.search-field .search-nav-buttons .nav-btn { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 4px; + background: transparent; +} + +.search-field .search-nav-buttons .nav-btn:hover { + background-color: var(--theme-toolbar-background-hover); +} + +.search-field .close-btn { + margin-inline-end: 4px; +} + +.search-field.big .close-btn { + margin-inline-end: 8px; +} + +.search-field .close-btn::-moz-focus-inner { + border: none; +} diff --git a/devtools/client/debugger/src/components/shared/SearchInput.js b/devtools/client/debugger/src/components/shared/SearchInput.js new file mode 100644 index 0000000000..896f9caf9e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SearchInput.js @@ -0,0 +1,272 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React, { Component } from "react"; + +import { CloseButton } from "./Button"; + +import AccessibleImage from "./AccessibleImage"; +import classnames from "classnames"; +import "./SearchInput.css"; + +const arrowBtn = (onClick, type, className, tooltip) => { + const props = { + className, + key: type, + onClick, + title: tooltip, + type, + }; + + return ( + <button {...props}> + <AccessibleImage className={type} /> + </button> + ); +}; + +type Props = { + count: number, + expanded: boolean, + handleClose?: (e: SyntheticMouseEvent<HTMLDivElement>) => void, + handleNext?: (e: SyntheticMouseEvent<HTMLButtonElement>) => void, + handlePrev?: (e: SyntheticMouseEvent<HTMLButtonElement>) => void, + hasPrefix?: boolean, + onBlur?: (e: SyntheticFocusEvent<HTMLInputElement>) => void, + onChange: (e: SyntheticInputEvent<HTMLInputElement>) => void, + onFocus?: (e: SyntheticFocusEvent<HTMLInputElement>) => void, + onKeyDown: (e: SyntheticKeyboardEvent<HTMLInputElement>) => void, + onKeyUp?: (e: SyntheticKeyboardEvent<HTMLInputElement>) => void, + onHistoryScroll?: (historyValue: string) => void, + placeholder: string, + query: string, + selectedItemId?: string, + shouldFocus?: boolean, + showErrorEmoji: boolean, + size: string, + summaryMsg: string, + showClose: boolean, + isLoading: boolean, +}; + +type State = { + history: Array<string>, +}; + +class SearchInput extends Component<Props, State> { + displayName: "SearchInput"; + $input: ?HTMLInputElement; + + static defaultProps = { + expanded: false, + hasPrefix: false, + selectedItemId: "", + size: "", + showClose: true, + }; + + constructor(props: Props) { + super(props); + + this.state = { + history: [], + }; + } + + componentDidMount() { + this.setFocus(); + } + + componentDidUpdate(prevProps: Props) { + if (this.props.shouldFocus && !prevProps.shouldFocus) { + this.setFocus(); + } + } + + setFocus() { + if (this.$input) { + const input = this.$input; + input.focus(); + + if (!input.value) { + return; + } + + // omit prefix @:# from being selected + const selectStartPos = this.props.hasPrefix ? 1 : 0; + input.setSelectionRange(selectStartPos, input.value.length + 1); + } + } + + renderSvg() { + return <AccessibleImage className="search" />; + } + + renderArrowButtons() { + const { handleNext, handlePrev } = this.props; + + return [ + arrowBtn( + handlePrev, + "arrow-up", + classnames("nav-btn", "prev"), + L10N.getFormatStr("editor.searchResults.prevResult") + ), + arrowBtn( + handleNext, + "arrow-down", + classnames("nav-btn", "next"), + L10N.getFormatStr("editor.searchResults.nextResult") + ), + ]; + } + + onFocus = (e: SyntheticFocusEvent<HTMLInputElement>) => { + const { onFocus } = this.props; + + if (onFocus) { + onFocus(e); + } + }; + + onBlur = (e: SyntheticFocusEvent<HTMLInputElement>) => { + const { onBlur } = this.props; + + if (onBlur) { + onBlur(e); + } + }; + + onKeyDown = (e: any) => { + const { onHistoryScroll, onKeyDown } = this.props; + if (!onHistoryScroll) { + return onKeyDown(e); + } + + const inputValue = e.target.value; + const { history } = this.state; + const currentHistoryIndex = history.indexOf(inputValue); + + if (e.key === "Enter") { + this.saveEnteredTerm(inputValue); + return onKeyDown(e); + } + + if (e.key === "ArrowUp") { + const previous = + currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1; + const previousInHistory = history[previous]; + if (previousInHistory) { + e.preventDefault(); + onHistoryScroll(previousInHistory); + } + return; + } + + if (e.key === "ArrowDown") { + const next = currentHistoryIndex + 1; + const nextInHistory = history[next]; + if (nextInHistory) { + onHistoryScroll(nextInHistory); + } + } + }; + + saveEnteredTerm(query: string) { + const { history } = this.state; + const previousIndex = history.indexOf(query); + if (previousIndex !== -1) { + history.splice(previousIndex, 1); + } + history.push(query); + this.setState({ history }); + } + + renderSummaryMsg() { + const { summaryMsg } = this.props; + + if (!summaryMsg) { + return null; + } + + return <div className="search-field-summary">{summaryMsg}</div>; + } + + renderSpinner() { + const { isLoading } = this.props; + if (isLoading) { + return <AccessibleImage className="loader spin" />; + } + } + + renderNav() { + const { count, handleNext, handlePrev } = this.props; + if ((!handleNext && !handlePrev) || !count || count == 1) { + return; + } + + return ( + <div className="search-nav-buttons">{this.renderArrowButtons()}</div> + ); + } + + render() { + const { + expanded, + handleClose, + onChange, + onKeyUp, + placeholder, + query, + selectedItemId, + showErrorEmoji, + size, + showClose, + } = this.props; + + const inputProps = { + className: classnames({ + empty: showErrorEmoji, + }), + onChange, + onKeyDown: e => this.onKeyDown(e), + onKeyUp, + onFocus: e => this.onFocus(e), + onBlur: e => this.onBlur(e), + "aria-autocomplete": "list", + "aria-controls": "result-list", + "aria-activedescendant": + expanded && selectedItemId ? `${selectedItemId}-title` : "", + placeholder, + value: query, + spellCheck: false, + ref: c => (this.$input = c), + }; + + return ( + <div className="search-outline"> + <div + className={classnames("search-field", size)} + role="combobox" + aria-haspopup="listbox" + aria-owns="result-list" + aria-expanded={expanded} + > + {this.renderSvg()} + <input {...inputProps} /> + {this.renderSpinner()} + {this.renderSummaryMsg()} + {this.renderNav()} + {showClose && ( + <CloseButton handleClick={handleClose} buttonClass={size} /> + )} + </div> + </div> + ); + } +} + +export default SearchInput; diff --git a/devtools/client/debugger/src/components/shared/SmartGap.js b/devtools/client/debugger/src/components/shared/SmartGap.js new file mode 100644 index 0000000000..ce26c0d35e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SmartGap.js @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import type { Coords } from "./Popover"; + +type Props = { + token: HTMLDivElement, + preview: ?HTMLDivElement, + type: ?string, + gapHeight: number, + coords: Coords, + offset: number, +}; + +function shorten(coordinates) { + // In cases where the token is wider than the preview, the smartGap + // gets distorted. This shortens the coordinate array so that the smartGap + // is only touching 2 corners of the token (instead of all 4 corners) + coordinates.splice(0, 2); + coordinates.splice(4, 2); + return coordinates; +} + +function getSmartGapCoordinates( + preview: ClientRect, + token: ClientRect, + offset: number, + orientation: string, + gapHeight: number, + coords: Coords +) { + if (orientation === "up") { + const coordinates = [ + token.left - coords.left + offset, + token.top + token.height - (coords.top + preview.height) + gapHeight, + 0, + 0, + preview.width + offset, + 0, + token.left + token.width - coords.left + offset, + token.top + token.height - (coords.top + preview.height) + gapHeight, + token.left + token.width - coords.left + offset, + token.top - (coords.top + preview.height) + gapHeight, + token.left - coords.left + offset, + token.top - (coords.top + preview.height) + gapHeight, + ]; + return preview.width > token.width ? coordinates : shorten(coordinates); + } + if (orientation === "down") { + const coordinates = [ + token.left + token.width - (coords.left + preview.top) + offset, + 0, + preview.width + offset, + coords.top - token.top + gapHeight, + 0, + coords.top - token.top + gapHeight, + token.left - (coords.left + preview.top) + offset, + 0, + token.left - (coords.left + preview.top) + offset, + token.height, + token.left + token.width - (coords.left + preview.top) + offset, + token.height, + ]; + return preview.width > token.width ? coordinates : shorten(coordinates); + } + return [ + 0, + token.top - coords.top, + gapHeight + token.width, + 0, + gapHeight + token.width, + preview.height - gapHeight, + 0, + token.top + token.height - coords.top, + token.width, + token.top + token.height - coords.top, + token.width, + token.top - coords.top, + ]; +} + +function getSmartGapDimensions( + previewRect: ClientRect, + tokenRect: ClientRect, + offset: number, + orientation: string, + gapHeight: number, + coords: Coords +) { + if (orientation === "up") { + return { + height: + tokenRect.top + + tokenRect.height - + coords.top - + previewRect.height + + gapHeight, + width: Math.max(previewRect.width, tokenRect.width) + offset, + }; + } + if (orientation === "down") { + return { + height: coords.top - tokenRect.top + gapHeight, + width: Math.max(previewRect.width, tokenRect.width) + offset, + }; + } + return { + height: previewRect.height - gapHeight, + width: coords.left - tokenRect.left + gapHeight, + }; +} + +export default function SmartGap({ + token, + preview, + type, + gapHeight, + coords, + offset, +}: Props) { + const tokenRect = token.getBoundingClientRect(); + // $FlowIgnore + const previewRect = preview.getBoundingClientRect(); + const { orientation } = coords; + let optionalMarginLeft, optionalMarginTop; + + if (orientation === "down") { + optionalMarginTop = -tokenRect.height; + } else if (orientation === "right") { + optionalMarginLeft = -tokenRect.width; + } + + const { height, width } = getSmartGapDimensions( + previewRect, + tokenRect, + -offset, + orientation, + gapHeight, + coords + ); + const coordinates = getSmartGapCoordinates( + previewRect, + tokenRect, + -offset, + orientation, + gapHeight, + coords + ); + + return ( + <svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + style={{ + height, + width, + position: "absolute", + marginLeft: optionalMarginLeft, + marginTop: optionalMarginTop, + }} + > + <polygon points={coordinates} fill="transparent" /> + </svg> + ); +} diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.css b/devtools/client/debugger/src/components/shared/SourceIcon.css new file mode 100644 index 0000000000..e7a9f53ff6 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SourceIcon.css @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +/** + * Variant of AccessibleImage used in sources list and tabs. + * Define the different source type / framework / library icons here. + */ + +.source-icon { + margin-inline-end: 4px; +} + +/* Icons for frameworks and libs */ + +.img.aframe { + background-image: url(chrome://devtools/content/debugger/images/sources/aframe.svg); + background-color: transparent !important; +} + +.img.angular { + background-image: url(chrome://devtools/content/debugger/images/sources/angular.svg); + background-color: transparent !important; +} + +.img.babel { + mask-image: url(chrome://devtools/content/debugger/images/sources/babel.svg); +} + +.img.backbone { + mask-image: url(chrome://devtools/content/debugger/images/sources/backbone.svg); +} + +.img.choo { + background-image: url(chrome://devtools/content/debugger/images/sources/choo.svg); + background-color: transparent !important; +} + +.img.coffeescript { + mask-image: url(chrome://devtools/content/debugger/images/sources/coffeescript.svg); +} + +.img.dojo { + background-image: url(chrome://devtools/content/debugger/images/sources/dojo.svg); + background-color: transparent !important; +} + +.img.ember { + background-image: url(chrome://devtools/content/debugger/images/sources/ember.svg); + background-color: transparent !important; +} + +.img.express { + mask-image: url(chrome://devtools/content/debugger/images/sources/express.svg); +} + +.img.extension { + mask-image: url(chrome://devtools/content/debugger/images/sources/extension.svg); +} + +.img.immutable { + mask-image: url(chrome://devtools/content/debugger/images/sources/immutable.svg); +} + +.img.javascript { + mask-image: url(chrome://devtools/content/debugger/images/sources/javascript.svg); + mask-size: 14px 14px; +} + +.img.jquery { + mask-image: url(chrome://devtools/content/debugger/images/sources/jquery.svg); +} + +.img.lodash { + mask-image: url(chrome://devtools/content/debugger/images/sources/lodash.svg); +} + +.img.marko { + background-image: url(chrome://devtools/content/debugger/images/sources/marko.svg); + background-color: transparent !important; +} + +.img.mobx { + background-image: url(chrome://devtools/content/debugger/images/sources/mobx.svg); + background-color: transparent !important; +} + +.img.nextjs { + background-image: url(chrome://devtools/content/debugger/images/sources/nextjs.svg); + background-color: transparent !important; +} + +.img.node { + background-image: url(chrome://devtools/content/debugger/images/sources/node.svg); + background-color: transparent !important; +} + +.img.nuxtjs { + background-image: url(chrome://devtools/content/debugger/images/sources/nuxtjs.svg); + background-color: transparent !important; +} + +.img.preact { + background-image: url(chrome://devtools/content/debugger/images/sources/preact.svg); + background-color: transparent !important; +} + +.img.pug { + background-image: url(chrome://devtools/content/debugger/images/sources/pug.svg); + background-color: transparent !important; +} + +.img.react { + mask-image: url(chrome://devtools/content/debugger/images/sources/react.svg); + background-color: var(--theme-highlight-bluegrey); +} + +.img.redux { + mask-image: url(chrome://devtools/content/debugger/images/sources/redux.svg); +} + +.img.rxjs { + background-image: url(chrome://devtools/content/debugger/images/sources/rxjs.svg); + background-color: transparent !important; +} + +.img.sencha-extjs { + background-image: url(chrome://devtools/content/debugger/images/sources/sencha-extjs.svg); + background-color: transparent !important; +} + +.img.typescript { + mask-image: url(chrome://devtools/content/debugger/images/sources/typescript.svg); +} + +.img.underscore { + mask-image: url(chrome://devtools/content/debugger/images/sources/underscore.svg); +} + +/* We use both 'Vue' and 'VueJS' when identifying frameworks */ +.img.vue, +.img.vuejs { + background-image: url(chrome://devtools/content/debugger/images/sources/vuejs.svg); + background-color: transparent !important; +} + +.img.webpack { + background-image: url(chrome://devtools/content/debugger/images/sources/webpack.svg); + background-color: transparent !important; +} diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.js b/devtools/client/debugger/src/components/shared/SourceIcon.js new file mode 100644 index 0000000000..25e88ea69d --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SourceIcon.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React, { PureComponent } from "react"; + +import { connect } from "../../utils/connect"; + +import AccessibleImage from "./AccessibleImage"; + +import { getSourceClassnames, isPretty } from "../../utils/source"; +import { getFramework } from "../../utils/tabs"; +import { getSymbols, getTabs } from "../../selectors"; + +import type { Source } from "../../types"; +import type { Symbols } from "../../reducers/types"; + +import "./SourceIcon.css"; + +type OwnProps = {| + source: Source, + + // An additional validator for the icon returned + modifier?: string => string | null, +|}; +type Props = { + source: Source, + modifier?: string => string | null, + + // symbols will provide framework information + symbols: ?Symbols, + framework: ?string, +}; + +class SourceIcon extends PureComponent<Props> { + render() { + const { modifier, source, symbols, framework } = this.props; + let iconClass = ""; + + if (isPretty(source)) { + iconClass = "prettyPrint"; + } else { + iconClass = framework + ? framework.toLowerCase() + : getSourceClassnames(source, symbols); + } + + if (modifier) { + const modified = modifier(iconClass); + if (!modified) { + return null; + } + iconClass = modified; + } + + return <AccessibleImage className={`source-icon ${iconClass}`} />; + } +} + +export default connect<Props, OwnProps, _, _, _, _>((state, props) => ({ + symbols: getSymbols(state, props.source), + framework: getFramework(getTabs(state), props.source.url), +}))(SourceIcon); diff --git a/devtools/client/debugger/src/components/shared/menu.css b/devtools/client/debugger/src/components/shared/menu.css new file mode 100644 index 0000000000..579cdf5dd9 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/menu.css @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +menupopup { + position: fixed; + z-index: 10000; + background: white; + border: 1px solid #cccccc; + padding: 5px 0; + background: #f2f2f2; + border-radius: 5px; + color: #585858; + box-shadow: 0 0 4px 0 rgba(190, 190, 190, 0.8); + min-width: 130px; +} + +menuitem { + display: block; + padding: 0 20px; + line-height: 20px; + font-weight: 500; + font-size: 13px; + user-select: none; +} + +menuitem:hover { + background: #3780fb; + color: white; +} + +menuitem[disabled="true"] { + color: #cccccc; +} + +menuitem[disabled="true"]:hover { + background-color: transparent; + cursor: default; +} + +menuseparator { + border-bottom: 1px solid #cacdd3; + width: 100%; + height: 5px; + display: block; + margin-bottom: 5px; +} + +#contextmenu-mask.show { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} diff --git a/devtools/client/debugger/src/components/shared/moz.build b/devtools/client/debugger/src/components/shared/moz.build new file mode 100644 index 0000000000..97fc3e5465 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/moz.build @@ -0,0 +1,24 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "Button", +] + +CompiledModules( + "AccessibleImage.js", + "Accordion.js", + "Badge.js", + "BracketArrow.js", + "Dropdown.js", + "ManagedTree.js", + "Modal.js", + "Popover.js", + "PreviewFunction.js", + "ResultList.js", + "SearchInput.js", + "SourceIcon.js", + "SmartGap.js", +) diff --git a/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js new file mode 100644 index 0000000000..6a8ce2fb6e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import Accordion from "../Accordion"; + +describe("Accordion", () => { + const testItems = [ + { + header: "Test Accordion Item 1", + className: "accordion-item-1", + component: <div />, + opened: false, + onToggle: jest.fn(), + }, + { + header: "Test Accordion Item 2", + className: "accordion-item-2", + component: <div />, + buttons: <button />, + opened: false, + onToggle: jest.fn(), + }, + { + header: "Test Accordion Item 3", + className: "accordion-item-3", + component: <div />, + opened: true, + onToggle: jest.fn(), + }, + ]; + const wrapper = shallow(<Accordion items={testItems} />); + it("basic render", () => expect(wrapper).toMatchSnapshot()); + wrapper.find(".accordion-item-1 ._header").simulate("click"); + it("handleClick and onToggle", () => + expect(testItems[0].onToggle).toHaveBeenCalledWith(true)); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/Badge.spec.js b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js new file mode 100644 index 0000000000..ea13b89c83 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import Badge from "../Badge"; + +describe("Badge", () => { + it("render", () => expect(shallow(<Badge>{3}</Badge>)).toMatchSnapshot()); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js new file mode 100644 index 0000000000..b66f60f5f1 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import BracketArrow from "../BracketArrow"; + +describe("BracketArrow", () => { + const wrapper = shallow( + <BracketArrow orientation="down" left={10} top={20} bottom={50} /> + ); + it("render", () => expect(wrapper).toMatchSnapshot()); + it("render up", () => { + wrapper.setProps({ orientation: null }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js new file mode 100644 index 0000000000..66cfd5f91a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import Dropdown from "../Dropdown"; + +describe("Dropdown", () => { + const wrapper = shallow(<Dropdown panel={<div />} icon="✅" />); + it("render", () => expect(wrapper).toMatchSnapshot()); + wrapper.find(".dropdown").simulate("click"); + it("handle toggleDropdown", () => + expect(wrapper.state().dropdownShown).toEqual(true)); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js b/devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js new file mode 100644 index 0000000000..f647a46a65 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/ManagedTree.spec.js @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { mount, shallow } from "enzyme"; + +import ManagedTree from "../ManagedTree"; + +function getTestContent() { + const testTree = { + a: { + value: "FOO", + children: [ + { value: 1 }, + { value: 2 }, + { value: 3 }, + { value: 4 }, + { value: 5 }, + ], + }, + b: { + value: "BAR", + children: [ + { value: "A" }, + { value: "B" }, + { value: "C" }, + { value: "D" }, + { value: "E" }, + ], + }, + c: { value: "BAZ" }, + }; + const renderItem = item => <div>{item.value ? item.value : item}</div>; + const onFocus = jest.fn(); + const onExpand = jest.fn(); + const onCollapse = jest.fn(); + const getPath = (item, i) => { + if (item.value) { + return item.value; + } + if (i) { + return `${i}`; + } + return `${item}-$`; + }; + + return { + testTree, + props: { + getRoots: () => Object.keys(testTree), + getParent: item => null, + getChildren: branch => branch.children || [], + itemHeight: 24, + autoExpandAll: true, + autoExpandDepth: 1, + getPath, + renderItem, + onFocus, + onExpand, + onCollapse, + }, + }; +} + +describe("ManagedTree", () => { + it("render", () => + expect( + shallow(<ManagedTree {...getTestContent().props} />) + ).toMatchSnapshot()); + it("expands list items", () => { + const { props, testTree } = getTestContent(); + const wrapper = shallow(<ManagedTree {...props} />); + wrapper.setProps({ + listItems: testTree.b.children, + }); + expect(wrapper).toMatchSnapshot(); + }); + it("highlights list items", () => { + const { props, testTree } = getTestContent(); + const wrapper = shallow(<ManagedTree {...props} />); + wrapper.setProps({ + highlightItems: testTree.a.children, + }); + expect(wrapper).toMatchSnapshot(); + }); + + it("sets expanded items", () => { + const { props, testTree } = getTestContent(); + const wrapper = mount(<ManagedTree {...props} />); + expect(wrapper).toMatchSnapshot(); + // We auto-expanded the first layer, so unexpand first node. + wrapper + .find("TreeNode") + .first() + .simulate("click"); + expect(wrapper).toMatchSnapshot(); + expect(props.onExpand).toHaveBeenCalledWith( + "c", + new Set( + Object.keys(testTree) + .filter(i => i !== "a") + .map(k => `${k}-$`) + ) + ); + wrapper + .find("TreeNode") + .first() + .simulate("click"); + expect(props.onExpand).toHaveBeenCalledWith( + "c", + new Set(Object.keys(testTree).map(k => `${k}-$`)) + ); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/Modal.spec.js b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js new file mode 100644 index 0000000000..163f0bc654 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import { Modal } from "../Modal"; + +describe("Modal", () => { + it("renders", () => { + const wrapper = shallow(<Modal handleClose={() => {}} status="entering" />); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles close modal click", () => { + const handleCloseSpy = jest.fn(); + const wrapper = shallow( + <Modal handleClose={handleCloseSpy} status="entering" /> + ); + wrapper.find(".modal-wrapper").simulate("click"); + expect(handleCloseSpy).toHaveBeenCalled(); + }); + + it("renders children", () => { + const children = <div className="aChild" />; + const wrapper = shallow( + <Modal children={children} handleClose={() => {}} status="entering" /> + ); + expect(wrapper.find(".aChild")).toHaveLength(1); + }); + + it("passes additionalClass to child div class", () => { + const additionalClass = "testAddon"; + const wrapper = shallow( + <Modal + additionalClass={additionalClass} + handleClose={() => {}} + status="entering" + /> + ); + expect(wrapper.find(`.modal-wrapper .${additionalClass}`)).toHaveLength(1); + }); + + it("passes status to child div class", () => { + const status: any = "testStatus"; + const wrapper = shallow(<Modal status={status} handleClose={() => {}} />); + expect(wrapper.find(`.modal-wrapper .${status}`)).toHaveLength(1); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/Popover.spec.js b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js new file mode 100644 index 0000000000..8b3f312722 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { mount } from "enzyme"; + +import Popover from "../Popover"; + +describe("Popover", () => { + const onMouseLeave = jest.fn(); + const onKeyDown = jest.fn(); + const editorRef: any = { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 100, + top: 250, + right: 0, + bottom: 0, + left: 20, + }; + }, + }; + + const targetRef: any = { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 100, + top: 250, + right: 0, + bottom: 0, + left: 20, + }; + }, + }; + const targetPosition = { + x: 100, + y: 200, + width: 300, + height: 300, + top: 50, + right: 0, + bottom: 0, + left: 200, + }; + const popover = mount( + <Popover + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editorRef} + targetPosition={targetPosition} + mouseout={() => {}} + target={targetRef} + > + <h1>Poppy!</h1> + </Popover> + ); + + const tooltip = mount( + <Popover + type="tooltip" + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editorRef} + targetPosition={targetPosition} + mouseout={() => {}} + target={targetRef} + > + <h1>Toolie!</h1> + </Popover> + ); + + beforeEach(() => { + onMouseLeave.mockClear(); + onKeyDown.mockClear(); + }); + + it("render", () => expect(popover).toMatchSnapshot()); + + it("render (tooltip)", () => expect(tooltip).toMatchSnapshot()); + + it("mount popover", () => { + const mountedPopover = mount( + <Popover + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editorRef} + targetPosition={targetPosition} + mouseout={() => {}} + target={targetRef} + > + <h1>Poppy!</h1> + </Popover> + ); + expect(mountedPopover).toMatchSnapshot(); + }); + + it("mount tooltip", () => { + const mountedTooltip = mount( + <Popover + type="tooltip" + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editorRef} + targetPosition={targetPosition} + mouseout={() => {}} + target={targetRef} + > + <h1>Toolie!</h1> + </Popover> + ); + expect(mountedTooltip).toMatchSnapshot(); + }); + + it("tooltip normally displays above the target", () => { + const editor: any = { + getBoundingClientRect() { + return { + width: 500, + height: 500, + top: 0, + bottom: 500, + left: 0, + right: 500, + }; + }, + }; + const target = { + width: 30, + height: 10, + top: 100, + bottom: 110, + left: 20, + right: 50, + }; + + const mountedTooltip = mount( + <Popover + type="tooltip" + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editor} + targetPosition={target} + mouseout={() => {}} + target={targetRef} + > + <h1>Toolie!</h1> + </Popover> + ); + + const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10); + expect(toolTipTop).toBeLessThanOrEqual(target.top); + }); + + it("tooltop won't display above the target when insufficient space", () => { + const editor: any = { + getBoundingClientRect() { + return { + width: 100, + height: 100, + top: 0, + bottom: 100, + left: 0, + right: 100, + }; + }, + }; + const target = { + width: 30, + height: 10, + top: 0, + bottom: 10, + left: 20, + right: 50, + }; + + const mountedTooltip = mount( + <Popover + type="tooltip" + onMouseLeave={onMouseLeave} + onKeyDown={onKeyDown} + editorRef={editor} + targetPosition={target} + mouseout={() => {}} + target={targetRef} + > + <h1>Toolie!</h1> + </Popover> + ); + + const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10); + expect(toolTipTop).toBeGreaterThanOrEqual(target.bottom); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js new file mode 100644 index 0000000000..06897683c0 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; +import PreviewFunction from "../PreviewFunction"; + +function render(props) { + return shallow(<PreviewFunction {...props} />, { context: { l10n: L10N } }); +} + +describe("PreviewFunction", () => { + it("should return a span", () => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + expect(returnedSpan).toMatchSnapshot(); + expect(returnedSpan.name()).toEqual("span"); + }); + + it('should return a span with a class of "function-signature"', () => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + expect(returnedSpan.hasClass("function-signature")).toBe(true); + }); + + it("should return a span with 3 children", () => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + expect(returnedSpan.children()).toHaveLength(3); + }); + + describe("function name", () => { + it("should be a span", () => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + expect( + returnedSpan + .children() + .first() + .name() + ).toEqual("span"); + }); + + it('should have a "function-name" class', () => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + expect( + returnedSpan + .children() + .first() + .hasClass("function-name") + ).toBe(true); + }); + + it("should be be set to userDisplayName if defined", () => { + const item = { + name: "", + userDisplayName: "chuck", + displayName: "norris", + }; + const returnedSpan = render({ func: item }); + expect( + returnedSpan + .children() + .first() + .first() + .text() + ).toEqual("chuck"); + }); + + it('should use displayName if defined & no "userDisplayName" exist', () => { + const item = { + displayName: "norris", + name: "last", + }; + const returnedSpan = render({ func: item }); + expect( + returnedSpan + .children() + .first() + .first() + .text() + ).toEqual("norris"); + }); + + it('should use to name if no "userDisplayName"/"displayName" exist', () => { + const item = { + name: "last", + }; + const returnedSpan = render({ func: item }); + expect( + returnedSpan + .children() + .first() + .first() + .text() + ).toEqual("last"); + }); + }); + + describe("render parentheses", () => { + let leftParen; + let rightParen; + + beforeAll(() => { + const item = { name: "" }; + const returnedSpan = render({ func: item }); + const children = returnedSpan.children(); + leftParen = returnedSpan.childAt(1); + rightParen = returnedSpan.childAt(children.length - 1); + }); + + it("should be spans", () => { + expect(leftParen.name()).toEqual("span"); + expect(rightParen.name()).toEqual("span"); + }); + + it("should create a left paren", () => { + expect(leftParen.text()).toEqual("("); + }); + + it("should create a right paren", () => { + expect(rightParen.text()).toEqual(")"); + }); + }); + + describe("render parameters", () => { + let returnedSpan; + let children; + + beforeAll(() => { + const item = { + name: "", + parameterNames: ["one", "two", "three"], + }; + returnedSpan = render({ func: item }); + children = returnedSpan.children(); + }); + + it("should render spans according to the dynamic params given", () => { + expect(children).toHaveLength(8); + }); + + it("should render the parameters names", () => { + expect(returnedSpan.childAt(2).text()).toEqual("one"); + }); + + it("should render the parameters commas", () => { + expect(returnedSpan.childAt(3).text()).toEqual(", "); + }); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js new file mode 100644 index 0000000000..242e4a7aec --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; +import ResultList from "../ResultList"; + +const selectItem = jest.fn(); +const selectedIndex = 1; +const payload = { + items: [ + { + id: 0, + subtitle: "subtitle", + title: "title", + value: "value", + }, + { + id: 1, + subtitle: "subtitle 1", + title: "title 1", + value: "value 1", + }, + ], + selected: selectedIndex, + selectItem, +}; + +describe("Result list", () => { + it("should call onClick function", () => { + const wrapper = shallow(<ResultList {...payload} />); + + wrapper.childAt(selectedIndex).simulate("click"); + expect(selectItem).toHaveBeenCalled(); + }); + + it("should render the component", () => { + const wrapper = shallow(<ResultList {...payload} />); + expect(wrapper).toMatchSnapshot(); + }); + + it("selected index should have 'selected class'", () => { + const wrapper = shallow(<ResultList {...payload} />); + const childHasClass = wrapper.childAt(selectedIndex).hasClass("selected"); + + expect(childHasClass).toEqual(true); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js new file mode 100644 index 0000000000..357de84c4f --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// @flow + +import React from "react"; +import { shallow } from "enzyme"; + +import SearchInput from "../SearchInput"; + +describe("SearchInput", () => { + // !! wrapper is defined outside test scope + // so it will keep values between tests + const wrapper = shallow( + <SearchInput + query="" + count={5} + placeholder="A placeholder" + summaryMsg="So many results" + showErrorEmoji={false} + isLoading={false} + onChange={() => {}} + onKeyDown={() => {}} + /> + ); + + it("renders", () => expect(wrapper).toMatchSnapshot()); + + it("shows nav buttons", () => { + wrapper.setProps({ + handleNext: jest.fn(), + handlePrev: jest.fn(), + }); + expect(wrapper).toMatchSnapshot(); + }); + + it("shows svg error emoji", () => { + wrapper.setProps({ showErrorEmoji: true }); + expect(wrapper).toMatchSnapshot(); + }); + + it("shows svg magnifying glass", () => { + wrapper.setProps({ showErrorEmoji: false }); + expect(wrapper).toMatchSnapshot(); + }); + + describe("with optional onHistoryScroll", () => { + const searches = ["foo", "bar", "baz"]; + const createSearch = term => ({ + target: { value: term }, + key: "Enter", + }); + + const scrollUp = currentTerm => ({ + key: "ArrowUp", + target: { value: currentTerm }, + preventDefault: jest.fn(), + }); + const scrollDown = currentTerm => ({ + key: "ArrowDown", + target: { value: currentTerm }, + preventDefault: jest.fn(), + }); + + it("stores entered history in state", () => { + wrapper.setProps({ + onHistoryScroll: jest.fn(), + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + }); + + it("stores scroll history in state", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + expect(wrapper.state().history[1]).toEqual(searches[1]); + }); + + it("scrolls up stored history on arrow up", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[1])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + expect(wrapper.state().history[1]).toEqual(searches[1]); + expect(onHistoryScroll).toHaveBeenCalledWith(searches[0]); + }); + + it("scrolls down stored history on arrow down", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + wrapper.find("input").simulate("keyDown", createSearch(searches[2])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[2])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[1])); + wrapper.find("input").simulate("keyDown", scrollDown(searches[0])); + expect(onHistoryScroll.mock.calls[2][0]).toBe(searches[1]); + }); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap new file mode 100644 index 0000000000..7ab4ed1ee6 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Accordion basic render 1`] = ` +<ul + className="accordion" +> + <li + className="accordion-item-1" + key="0" + > + <h2 + className="_header" + onClick={[Function]} + onKeyDown={[Function]} + tabIndex="0" + > + <AccessibleImage + className="arrow expanded" + /> + <span + className="header-label" + > + Test Accordion Item 1 + </span> + </h2> + <div + className="_content" + > + <div /> + </div> + </li> + <li + className="accordion-item-2" + key="1" + > + <h2 + className="_header" + onClick={[Function]} + onKeyDown={[Function]} + tabIndex="0" + > + <AccessibleImage + className="arrow " + /> + <span + className="header-label" + > + Test Accordion Item 2 + </span> + <div + className="header-buttons" + tabIndex="-1" + > + <button /> + </div> + </h2> + </li> + <li + className="accordion-item-3" + key="2" + > + <h2 + className="_header" + onClick={[Function]} + onKeyDown={[Function]} + tabIndex="0" + > + <AccessibleImage + className="arrow expanded" + /> + <span + className="header-label" + > + Test Accordion Item 3 + </span> + </h2> + <div + className="_content" + > + <div /> + </div> + </li> +</ul> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap new file mode 100644 index 0000000000..cbeeeaa3f2 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badge render 1`] = ` +<span + className="badge text-white text-center" +> + 3 +</span> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap new file mode 100644 index 0000000000..5078cebc9e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BracketArrow render 1`] = ` +<div + className="bracket-arrow down" + style={ + Object { + "bottom": 50, + "left": 10, + "top": 20, + } + } +/> +`; + +exports[`BracketArrow render up 1`] = ` +<div + className="bracket-arrow up" + style={ + Object { + "bottom": 50, + "left": 10, + "top": 20, + } + } +/> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap new file mode 100644 index 0000000000..fd60784327 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dropdown render 1`] = ` +<div + className="dropdown-block" +> + <div + className="dropdown" + onClick={[Function]} + style={ + Object { + "display": "block", + } + } + > + <div /> + </div> + <button + className="dropdown-button" + onClick={[Function]} + > + ✅ + </button> + <div + className="dropdown-mask" + onClick={[Function]} + style={ + Object { + "display": "block", + } + } + /> +</div> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/ManagedTree.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ManagedTree.spec.js.snap new file mode 100644 index 0000000000..ef785a6398 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ManagedTree.spec.js.snap @@ -0,0 +1,543 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManagedTree expands list items 1`] = ` +<div + className="managed-tree" +> + <Tree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getKey={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + isExpanded={[Function]} + itemHeight={24} + listItems={ + Array [ + Object { + "value": "A", + }, + Object { + "value": "B", + }, + Object { + "value": "C", + }, + Object { + "value": "D", + }, + Object { + "value": "E", + }, + ] + } + onCollapse={[Function]} + onExpand={[Function]} + onFocus={ + [MockFunction] { + "calls": Array [ + Array [ + Object { + "value": "A", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + renderItem={[Function]} + /> +</div> +`; + +exports[`ManagedTree highlights list items 1`] = ` +<div + className="managed-tree" +> + <Tree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getKey={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + highlightItems={ + Array [ + Object { + "value": 5, + }, + Object { + "value": 4, + }, + Object { + "value": 3, + }, + Object { + "value": 2, + }, + Object { + "value": 1, + }, + ] + } + isExpanded={[Function]} + itemHeight={24} + onCollapse={[Function]} + onExpand={[Function]} + onFocus={ + [MockFunction] { + "calls": Array [ + Array [ + Object { + "value": 5, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + renderItem={[Function]} + /> +</div> +`; + +exports[`ManagedTree render 1`] = ` +<div + className="managed-tree" +> + <Tree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getKey={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + isExpanded={[Function]} + itemHeight={24} + onCollapse={[Function]} + onExpand={[Function]} + onFocus={[MockFunction]} + renderItem={[Function]} + /> +</div> +`; + +exports[`ManagedTree sets expanded items 1`] = ` +<ManagedTree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + itemHeight={24} + onCollapse={[MockFunction]} + onExpand={ + [MockFunction] { + "calls": Array [ + Array [ + "a", + Set { + "a-$", + "b-$", + "c-$", + }, + ], + Array [ + "b", + Set { + "a-$", + "b-$", + "c-$", + }, + ], + Array [ + "c", + Set { + "a-$", + "b-$", + "c-$", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + Object { + "isThrow": false, + "value": undefined, + }, + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + onFocus={[MockFunction]} + renderItem={[Function]} +> + <div + className="managed-tree" + > + <Tree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getKey={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + isExpanded={[Function]} + itemHeight={24} + onCollapse={[Function]} + onExpand={[Function]} + onFocus={[MockFunction]} + renderItem={[Function]} + > + <div + className="tree " + onBlur={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyPress={[Function]} + onKeyUp={[Function]} + role="tree" + style={Object {}} + tabIndex="0" + > + <TreeNode + active={false} + depth={0} + expanded={true} + focused={false} + id="a-$" + index={0} + isExpandable={false} + item="a" + key="a-$-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-expanded={true} + aria-level={1} + className="tree-node" + data-expandable={false} + id="a-$" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + a + </div> + </div> + </TreeNode> + <TreeNode + active={false} + depth={0} + expanded={true} + focused={false} + id="1" + index={1} + isExpandable={false} + item="b" + key="1-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-expanded={true} + aria-level={1} + className="tree-node" + data-expandable={false} + id="1" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + b + </div> + </div> + </TreeNode> + <TreeNode + active={false} + depth={0} + expanded={true} + focused={false} + id="2" + index={2} + isExpandable={false} + item="c" + key="2-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-expanded={true} + aria-level={1} + className="tree-node" + data-expandable={false} + id="2" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + c + </div> + </div> + </TreeNode> + </div> + </Tree> + </div> +</ManagedTree> +`; + +exports[`ManagedTree sets expanded items 2`] = ` +<ManagedTree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + itemHeight={24} + onCollapse={ + [MockFunction] { + "calls": Array [ + Array [ + "a", + Set { + "b-$", + "c-$", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + onExpand={ + [MockFunction] { + "calls": Array [ + Array [ + "a", + Set { + "b-$", + "c-$", + }, + ], + Array [ + "b", + Set { + "b-$", + "c-$", + }, + ], + Array [ + "c", + Set { + "b-$", + "c-$", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + Object { + "isThrow": false, + "value": undefined, + }, + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + onFocus={ + [MockFunction] { + "calls": Array [ + Array [ + "a", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + renderItem={[Function]} +> + <div + className="managed-tree" + > + <Tree + autoExpandAll={true} + autoExpandDepth={1} + getChildren={[Function]} + getKey={[Function]} + getParent={[Function]} + getPath={[Function]} + getRoots={[Function]} + isExpanded={[Function]} + itemHeight={24} + onCollapse={[Function]} + onExpand={[Function]} + onFocus={ + [MockFunction] { + "calls": Array [ + Array [ + "a", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], + } + } + renderItem={[Function]} + > + <div + className="tree " + onBlur={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyPress={[Function]} + onKeyUp={[Function]} + role="tree" + style={Object {}} + tabIndex="0" + > + <TreeNode + active={false} + depth={0} + expanded={false} + focused={false} + id="a-$" + index={0} + isExpandable={false} + item="a" + key="a-$-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-level={1} + className="tree-node" + data-expandable={false} + id="a-$" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + a + </div> + </div> + </TreeNode> + <TreeNode + active={false} + depth={0} + expanded={true} + focused={false} + id="1" + index={1} + isExpandable={false} + item="b" + key="1-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-expanded={true} + aria-level={1} + className="tree-node" + data-expandable={false} + id="1" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + b + </div> + </div> + </TreeNode> + <TreeNode + active={false} + depth={0} + expanded={true} + focused={false} + id="2" + index={2} + isExpandable={false} + item="c" + key="2-inactive" + onClick={[Function]} + onCollapse={[Function]} + onExpand={[Function]} + renderItem={[Function]} + > + <div + aria-expanded={true} + aria-level={1} + className="tree-node" + data-expandable={false} + id="2" + onClick={[Function]} + onKeyDownCapture={null} + role="treeitem" + > + <div> + c + </div> + </div> + </TreeNode> + </div> + </Tree> + </div> +</ManagedTree> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap new file mode 100644 index 0000000000..e9b9639749 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal renders 1`] = ` +<div + className="modal-wrapper" + onClick={[Function]} +> + <div + className="modal entering" + onClick={[Function]} + /> +</div> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap new file mode 100644 index 0000000000..1c3589a6f8 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap @@ -0,0 +1,549 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Popover mount popover 1`] = ` +<Popover + editorRef={ + Object { + "getBoundingClientRect": [Function], + } + } + mouseout={[Function]} + onKeyDown={[MockFunction]} + onMouseLeave={[MockFunction]} + target={ + Object { + "getBoundingClientRect": [Function], + } + } + targetPosition={ + Object { + "bottom": 0, + "height": 300, + "left": 200, + "right": 0, + "top": 50, + "width": 300, + "x": 100, + "y": 200, + } + } + type="popover" +> + <div + className="popover orientation-right" + style={ + Object { + "left": 500, + "top": -50, + } + } + > + <BracketArrow + left={-4} + orientation="left" + top={98} + > + <div + className="bracket-arrow left" + style={ + Object { + "bottom": undefined, + "left": -4, + "top": 98, + } + } + /> + </BracketArrow> + <div + className="gap" + key="gap" + > + <SmartGap + coords={ + Object { + "left": 500, + "orientation": "right", + "targetMid": Object { + "x": -14, + "y": 98, + }, + "top": -50, + } + } + gapHeight={0} + offset={0} + preview={ + <div + class="popover orientation-right" + style="top: -50px; left: 500px;" + > + <div + class="bracket-arrow left" + style="left: -4px; top: 98px;" + /> + <div + class="gap" + > + <svg + style="height: 0px; width: 480px; position: absolute; margin-left: -100px;" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points="0,300,100,0,100,0,0,400,100,400,100,300" + /> + </svg> + </div> + <h1> + Poppy! + </h1> + </div> + } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="popover" + > + <svg + style={ + Object { + "height": 0, + "marginLeft": -100, + "marginTop": undefined, + "position": "absolute", + "width": 480, + } + } + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points={ + Array [ + 0, + 300, + 100, + 0, + 100, + 0, + 0, + 400, + 100, + 400, + 100, + 300, + ] + } + /> + </svg> + </SmartGap> + </div> + <h1> + Poppy! + </h1> + </div> +</Popover> +`; + +exports[`Popover mount tooltip 1`] = ` +<Popover + editorRef={ + Object { + "getBoundingClientRect": [Function], + } + } + mouseout={[Function]} + onKeyDown={[MockFunction]} + onMouseLeave={[MockFunction]} + target={ + Object { + "getBoundingClientRect": [Function], + } + } + targetPosition={ + Object { + "bottom": 0, + "height": 300, + "left": 200, + "right": 0, + "top": 50, + "width": 300, + "x": 100, + "y": 200, + } + } + type="tooltip" +> + <div + className="tooltip orientation-down" + style={ + Object { + "left": -8, + "top": 0, + } + } + > + <div + className="gap" + key="gap" + > + <SmartGap + coords={ + Object { + "left": -8, + "orientation": "down", + "targetMid": Object { + "x": 0, + "y": 0, + }, + "top": 0, + } + } + gapHeight={0} + offset={0} + preview={ + <div + class="tooltip orientation-down" + style="top: 0px; left: -8px;" + > + <div + class="gap" + > + <svg + style="height: -250px; width: 100px; position: absolute; margin-top: -100px;" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points="0,-250,0,-250,28,100,128,100" + /> + </svg> + </div> + <h1> + Toolie! + </h1> + </div> + } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="tooltip" + > + <svg + style={ + Object { + "height": -250, + "marginLeft": undefined, + "marginTop": -100, + "position": "absolute", + "width": 100, + } + } + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points={ + Array [ + 0, + -250, + 0, + -250, + 28, + 100, + 128, + 100, + ] + } + /> + </svg> + </SmartGap> + </div> + <h1> + Toolie! + </h1> + </div> +</Popover> +`; + +exports[`Popover render (tooltip) 1`] = ` +<Popover + editorRef={ + Object { + "getBoundingClientRect": [Function], + } + } + mouseout={[Function]} + onKeyDown={[MockFunction]} + onMouseLeave={[MockFunction]} + target={ + Object { + "getBoundingClientRect": [Function], + } + } + targetPosition={ + Object { + "bottom": 0, + "height": 300, + "left": 200, + "right": 0, + "top": 50, + "width": 300, + "x": 100, + "y": 200, + } + } + type="tooltip" +> + <div + className="tooltip orientation-down" + style={ + Object { + "left": -8, + "top": 0, + } + } + > + <div + className="gap" + key="gap" + > + <SmartGap + coords={ + Object { + "left": -8, + "orientation": "down", + "targetMid": Object { + "x": 0, + "y": 0, + }, + "top": 0, + } + } + gapHeight={0} + offset={0} + preview={ + <div + class="tooltip orientation-down" + style="top: 0px; left: -8px;" + > + <div + class="gap" + > + <svg + style="height: -250px; width: 100px; position: absolute; margin-top: -100px;" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points="0,-250,0,-250,28,100,128,100" + /> + </svg> + </div> + <h1> + Toolie! + </h1> + </div> + } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="tooltip" + > + <svg + style={ + Object { + "height": -250, + "marginLeft": undefined, + "marginTop": -100, + "position": "absolute", + "width": 100, + } + } + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points={ + Array [ + 0, + -250, + 0, + -250, + 28, + 100, + 128, + 100, + ] + } + /> + </svg> + </SmartGap> + </div> + <h1> + Toolie! + </h1> + </div> +</Popover> +`; + +exports[`Popover render 1`] = ` +<Popover + editorRef={ + Object { + "getBoundingClientRect": [Function], + } + } + mouseout={[Function]} + onKeyDown={[MockFunction]} + onMouseLeave={[MockFunction]} + target={ + Object { + "getBoundingClientRect": [Function], + } + } + targetPosition={ + Object { + "bottom": 0, + "height": 300, + "left": 200, + "right": 0, + "top": 50, + "width": 300, + "x": 100, + "y": 200, + } + } + type="popover" +> + <div + className="popover orientation-right" + style={ + Object { + "left": 500, + "top": -50, + } + } + > + <BracketArrow + left={-4} + orientation="left" + top={98} + > + <div + className="bracket-arrow left" + style={ + Object { + "bottom": undefined, + "left": -4, + "top": 98, + } + } + /> + </BracketArrow> + <div + className="gap" + key="gap" + > + <SmartGap + coords={ + Object { + "left": 500, + "orientation": "right", + "targetMid": Object { + "x": -14, + "y": 98, + }, + "top": -50, + } + } + gapHeight={0} + offset={0} + preview={ + <div + class="popover orientation-right" + style="top: -50px; left: 500px;" + > + <div + class="bracket-arrow left" + style="left: -4px; top: 98px;" + /> + <div + class="gap" + > + <svg + style="height: 0px; width: 480px; position: absolute; margin-left: -100px;" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points="0,300,100,0,100,0,0,400,100,400,100,300" + /> + </svg> + </div> + <h1> + Poppy! + </h1> + </div> + } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="popover" + > + <svg + style={ + Object { + "height": 0, + "marginLeft": -100, + "marginTop": undefined, + "position": "absolute", + "width": 480, + } + } + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + <polygon + fill="transparent" + points={ + Array [ + 0, + 300, + 100, + 0, + 100, + 0, + 0, + 400, + 100, + 400, + 100, + 300, + ] + } + /> + </svg> + </SmartGap> + </div> + <h1> + Poppy! + </h1> + </div> +</Popover> +`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap new file mode 100644 index 0000000000..e766bd45aa --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PreviewFunction should return a span 1`] = ` +<span + className="function-signature" +> + <span + className="function-name" + > + <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..50fd748f47 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchInput renders 1`] = ` +<div + className="search-outline" +> + <div + aria-expanded={false} + aria-haspopup="listbox" + aria-owns="result-list" + className="search-field" + role="combobox" + > + <AccessibleImage + className="search" + /> + <input + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="result-list" + className="" + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="A placeholder" + spellCheck={false} + value="" + /> + <div + className="search-field-summary" + > + So many results + </div> + <CloseButton + buttonClass="" + /> + </div> +</div> +`; + +exports[`SearchInput shows nav buttons 1`] = ` +<div + className="search-outline" +> + <div + aria-expanded={false} + aria-haspopup="listbox" + aria-owns="result-list" + className="search-field" + role="combobox" + > + <AccessibleImage + className="search" + /> + <input + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="result-list" + className="" + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="A placeholder" + spellCheck={false} + value="" + /> + <div + className="search-field-summary" + > + So many results + </div> + <div + className="search-nav-buttons" + > + <button + className="nav-btn prev" + key="arrow-up" + onClick={[MockFunction]} + title="Previous result" + type="arrow-up" + > + <AccessibleImage + className="arrow-up" + /> + </button> + <button + className="nav-btn next" + key="arrow-down" + onClick={[MockFunction]} + title="Next result" + type="arrow-down" + > + <AccessibleImage + className="arrow-down" + /> + </button> + </div> + <CloseButton + buttonClass="" + /> + </div> +</div> +`; + +exports[`SearchInput shows svg error emoji 1`] = ` +<div + className="search-outline" +> + <div + aria-expanded={false} + aria-haspopup="listbox" + aria-owns="result-list" + className="search-field" + role="combobox" + > + <AccessibleImage + className="search" + /> + <input + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="result-list" + className="empty" + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="A placeholder" + spellCheck={false} + value="" + /> + <div + className="search-field-summary" + > + So many results + </div> + <div + className="search-nav-buttons" + > + <button + className="nav-btn prev" + key="arrow-up" + onClick={[MockFunction]} + title="Previous result" + type="arrow-up" + > + <AccessibleImage + className="arrow-up" + /> + </button> + <button + className="nav-btn next" + key="arrow-down" + onClick={[MockFunction]} + title="Next result" + type="arrow-down" + > + <AccessibleImage + className="arrow-down" + /> + </button> + </div> + <CloseButton + buttonClass="" + /> + </div> +</div> +`; + +exports[`SearchInput shows svg magnifying glass 1`] = ` +<div + className="search-outline" +> + <div + aria-expanded={false} + aria-haspopup="listbox" + aria-owns="result-list" + className="search-field" + role="combobox" + > + <AccessibleImage + className="search" + /> + <input + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="result-list" + className="" + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="A placeholder" + spellCheck={false} + value="" + /> + <div + className="search-field-summary" + > + So many results + </div> + <div + className="search-nav-buttons" + > + <button + className="nav-btn prev" + key="arrow-up" + onClick={[MockFunction]} + title="Previous result" + type="arrow-up" + > + <AccessibleImage + className="arrow-up" + /> + </button> + <button + className="nav-btn next" + key="arrow-down" + onClick={[MockFunction]} + title="Next result" + type="arrow-down" + > + <AccessibleImage + className="arrow-down" + /> + </button> + </div> + <CloseButton + buttonClass="" + /> + </div> +</div> +`; |