From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../src/components/shared/AccessibleImage.css | 201 ++++++++ .../src/components/shared/AccessibleImage.js | 21 + .../debugger/src/components/shared/Accordion.css | 107 ++++ .../debugger/src/components/shared/Accordion.js | 89 ++++ .../debugger/src/components/shared/Badge.css | 16 + .../client/debugger/src/components/shared/Badge.js | 30 ++ .../src/components/shared/BracketArrow.css | 64 +++ .../debugger/src/components/shared/BracketArrow.js | 28 ++ .../src/components/shared/Button/CloseButton.js | 30 ++ .../components/shared/Button/CommandBarButton.js | 55 +++ .../components/shared/Button/PaneToggleButton.js | 58 +++ .../debugger/src/components/shared/Button/index.js | 9 + .../src/components/shared/Button/moz.build | 15 + .../shared/Button/styles/CloseButton.css | 35 ++ .../shared/Button/styles/CommandBarButton.css | 73 +++ .../shared/Button/styles/PaneToggleButton.css | 29 ++ .../src/components/shared/Button/styles/moz.build | 8 + .../shared/Button/tests/CloseButton.spec.js | 31 ++ .../shared/Button/tests/CommandBarButton.spec.js | 44 ++ .../shared/Button/tests/PaneToggleButton.spec.js | 51 ++ .../tests/__snapshots__/CloseButton.spec.js.snap | 13 + .../__snapshots__/CommandBarButton.spec.js.snap | 18 + .../__snapshots__/PaneToggleButton.spec.js.snap | 13 + .../debugger/src/components/shared/Dropdown.css | 97 ++++ .../debugger/src/components/shared/Dropdown.js | 74 +++ .../debugger/src/components/shared/Modal.css | 47 ++ .../client/debugger/src/components/shared/Modal.js | 45 ++ .../debugger/src/components/shared/Popover.css | 32 ++ .../debugger/src/components/shared/Popover.js | 324 ++++++++++++ .../src/components/shared/PreviewFunction.css | 23 + .../src/components/shared/PreviewFunction.js | 108 ++++ .../debugger/src/components/shared/ResultList.css | 131 +++++ .../debugger/src/components/shared/ResultList.js | 102 ++++ .../debugger/src/components/shared/SearchInput.css | 223 +++++++++ .../debugger/src/components/shared/SearchInput.js | 362 ++++++++++++++ .../debugger/src/components/shared/SmartGap.js | 170 +++++++ .../debugger/src/components/shared/SourceIcon.css | 176 +++++++ .../debugger/src/components/shared/SourceIcon.js | 71 +++ .../client/debugger/src/components/shared/menu.css | 55 +++ .../debugger/src/components/shared/moz.build | 23 + .../src/components/shared/tests/Accordion.spec.js | 47 ++ .../src/components/shared/tests/Badge.spec.js | 19 + .../components/shared/tests/BracketArrow.spec.js | 24 + .../src/components/shared/tests/Dropdown.spec.js | 21 + .../src/components/shared/tests/Modal.spec.js | 56 +++ .../src/components/shared/tests/Popover.spec.js | 212 ++++++++ .../shared/tests/PreviewFunction.spec.js | 130 +++++ .../src/components/shared/tests/ResultList.spec.js | 48 ++ .../components/shared/tests/SearchInput.spec.js | 126 +++++ .../tests/__snapshots__/Accordion.spec.js.snap | 81 +++ .../shared/tests/__snapshots__/Badge.spec.js.snap | 9 + .../tests/__snapshots__/BracketArrow.spec.js.snap | 27 + .../tests/__snapshots__/Dropdown.spec.js.snap | 34 ++ .../shared/tests/__snapshots__/Modal.spec.js.snap | 13 + .../tests/__snapshots__/Popover.spec.js.snap | 549 +++++++++++++++++++++ .../__snapshots__/PreviewFunction.spec.js.snap | 23 + .../tests/__snapshots__/ResultList.spec.js.snap | 55 +++ .../tests/__snapshots__/SearchInput.spec.js.snap | 267 ++++++++++ 58 files changed, 4842 insertions(+) create mode 100644 devtools/client/debugger/src/components/shared/AccessibleImage.css create mode 100644 devtools/client/debugger/src/components/shared/AccessibleImage.js create mode 100644 devtools/client/debugger/src/components/shared/Accordion.css create mode 100644 devtools/client/debugger/src/components/shared/Accordion.js create mode 100644 devtools/client/debugger/src/components/shared/Badge.css create mode 100644 devtools/client/debugger/src/components/shared/Badge.js create mode 100644 devtools/client/debugger/src/components/shared/BracketArrow.css create mode 100644 devtools/client/debugger/src/components/shared/BracketArrow.js create mode 100644 devtools/client/debugger/src/components/shared/Button/CloseButton.js create mode 100644 devtools/client/debugger/src/components/shared/Button/CommandBarButton.js create mode 100644 devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js create mode 100644 devtools/client/debugger/src/components/shared/Button/index.js create mode 100644 devtools/client/debugger/src/components/shared/Button/moz.build create mode 100644 devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css create mode 100644 devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css create mode 100644 devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css create mode 100644 devtools/client/debugger/src/components/shared/Button/styles/moz.build create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/Dropdown.css create mode 100644 devtools/client/debugger/src/components/shared/Dropdown.js create mode 100644 devtools/client/debugger/src/components/shared/Modal.css create mode 100644 devtools/client/debugger/src/components/shared/Modal.js create mode 100644 devtools/client/debugger/src/components/shared/Popover.css create mode 100644 devtools/client/debugger/src/components/shared/Popover.js create mode 100644 devtools/client/debugger/src/components/shared/PreviewFunction.css create mode 100644 devtools/client/debugger/src/components/shared/PreviewFunction.js create mode 100644 devtools/client/debugger/src/components/shared/ResultList.css create mode 100644 devtools/client/debugger/src/components/shared/ResultList.js create mode 100644 devtools/client/debugger/src/components/shared/SearchInput.css create mode 100644 devtools/client/debugger/src/components/shared/SearchInput.js create mode 100644 devtools/client/debugger/src/components/shared/SmartGap.js create mode 100644 devtools/client/debugger/src/components/shared/SourceIcon.css create mode 100644 devtools/client/debugger/src/components/shared/SourceIcon.js create mode 100644 devtools/client/debugger/src/components/shared/menu.css create mode 100644 devtools/client/debugger/src/components/shared/moz.build create mode 100644 devtools/client/debugger/src/components/shared/tests/Accordion.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/Badge.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/Modal.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/Popover.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/ResultList.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap create mode 100644 devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap (limited to 'devtools/client/debugger/src/components/shared') 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..4ba5f1326a --- /dev/null +++ b/devtools/client/debugger/src/components/shared/AccessibleImage.css @@ -0,0 +1,201 @@ +/* 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 . */ + +.img { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + /* use background-color for the icon color, and mask-image for its shape */ + background-color: var(--theme-icon-color); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + /* multicolor icons use background-image */ + background-position: center; + background-repeat: no-repeat; + background-size: contain; + /* do not let images shrink when used as flex children */ + flex-shrink: 0; +} + +/* Expand arrow icon */ +.img.arrow { + width: 10px; + height: 10px; + mask-image: url(chrome://devtools/content/debugger/images/arrow.svg); + /* we may override the width/height in specific contexts to make the + clickable area bigger, but we should always keep the mask size 10x10 */ + mask-size: 10px 10px; + background-color: var(--theme-icon-dimmed-color); + transform: rotate(-90deg); + transition: transform 180ms var(--animation-curve); +} + +.img.arrow:dir(rtl) { + transform: rotate(90deg); +} + +.img.arrow.expanded { + /* icon should always point to the bottom (default) when expanded, + regardless of the text direction */ + transform: none !important; +} + +.img.arrow-down { + mask-image: url(chrome://devtools/content/debugger/images/arrow-down.svg); +} + +.img.arrow-up { + mask-image: url(chrome://devtools/content/debugger/images/arrow-up.svg); +} + +.img.blackBox { + mask-image: url(chrome://devtools/content/debugger/images/blackBox.svg); +} + +.img.breadcrumb { + mask-image: url(chrome://devtools/content/debugger/images/breadcrumbs-divider.svg); +} + +.img.close { + mask-image: url(chrome://devtools/skin/images/close.svg); +} + +.img.disable-pausing { + mask-image: url(chrome://devtools/content/debugger/images/disable-pausing.svg); +} + +.img.enable-pausing { + mask-image: url(chrome://devtools/content/debugger/images/enable-pausing.svg); + background-color: var(--theme-icon-checked-color); +} + +.img.globe { + mask-image: url(chrome://devtools/content/debugger/images/globe.svg); +} + +.img.globe-small { + mask-image: url(chrome://devtools/content/debugger/images/globe-small.svg); + mask-size: 12px 12px; +} + +.img.window { + mask-image: url(chrome://devtools/content/debugger/images/window.svg); +} + +.img.file { + mask-image: url(chrome://devtools/content/debugger/images/file-small.svg); + mask-size: 12px 12px; +} + +.img.folder { + mask-image: url(chrome://devtools/content/debugger/images/folder.svg); +} + +.img.home { + mask-image: url(chrome://devtools/content/debugger/images/home.svg); +} + +.img.info { + mask-image: url(chrome://devtools/skin/images/info.svg); +} + +.img.loader { + background-image: url(chrome://devtools/content/debugger/images/loader.svg); + -moz-context-properties: fill; + fill: var(--theme-icon-color); + background-color: unset; +} + +.img.more-tabs { + mask-image: url(chrome://devtools/content/debugger/images/command-chevron.svg); +} + +html[dir="rtl"] .img.more-tabs { + transform: scaleX(-1); +} + +.img.sourcemap { + background-image: url(chrome://devtools/content/debugger/images/sourcemap.svg); + -moz-context-properties: fill; + fill: var(--theme-icon-warning-color); + background-color: unset; +} + +.img.next { + mask-image: url(chrome://devtools/content/debugger/images/next.svg); +} + +.img.next-circle { + mask-image: url(chrome://devtools/content/debugger/images/next-circle.svg); +} + +.img.pane-collapse { + mask-image: url(chrome://devtools/content/debugger/images/pane-collapse.svg); +} + +.img.pane-expand { + mask-image: url(chrome://devtools/content/debugger/images/pane-expand.svg); +} + +.img.pause { + mask-image: url(chrome://devtools/content/debugger/images/pause.svg); +} + +.img.plus { + mask-image: url(chrome://devtools/skin/images/add.svg); +} + +.img.prettyPrint { + background-image: url(chrome://devtools/content/debugger/images/prettyPrint.svg); + background-size: 14px 14px; + background-color: transparent !important; + fill: var(--theme-icon-color); + -moz-context-properties: fill; +} + +.img.removeAll { + mask-image: url(chrome://devtools/skin/images/clear.svg) +} + +.img.refresh { + mask-image: url(chrome://devtools/skin/images/reload.svg); +} + +.img.resume { + mask-image: url(resource://devtools-shared-images/resume.svg); +} + +.img.search { + mask-image: url(chrome://devtools/content/debugger/images/search.svg); +} + +.img.shortcuts { + mask-image: url(chrome://devtools/content/debugger/images/help.svg); +} + +.img.spin { + animation: spin 0.5s linear infinite; +} + +.img.stepIn { + mask-image: url(chrome://devtools/content/debugger/images/stepIn.svg); +} + +.img.stepOut { + mask-image: url(chrome://devtools/content/debugger/images/stepOut.svg); +} + +.img.stepOver { + mask-image: url(resource://devtools-shared-images/stepOver.svg); +} + +.img.tab { + mask-image: url(chrome://devtools/content/debugger/images/tab.svg); +} + +.img.worker { + mask-image: url(chrome://devtools/content/debugger/images/worker.svg); +} diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.js b/devtools/client/debugger/src/components/shared/AccessibleImage.js new file mode 100644 index 0000000000..e3a59573ea --- /dev/null +++ b/devtools/client/debugger/src/components/shared/AccessibleImage.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 . */ + +import React from "devtools/client/shared/vendor/react"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +const AccessibleImage = props => { + return React.createElement("span", { + ...props, + className: classnames("img", props.className), + }); +}; + +AccessibleImage.propTypes = { + className: PropTypes.string.isRequired, +}; + +export default AccessibleImage; diff --git a/devtools/client/debugger/src/components/shared/Accordion.css b/devtools/client/debugger/src/components/shared/Accordion.css new file mode 100644 index 0000000000..d970527014 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Accordion.css @@ -0,0 +1,107 @@ +/* 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 . */ + +.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; + column-gap: 8px; + 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 .header-label { + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--theme-toolbar-color); + background: transparent; + padding: 0; + + /* align expand arrow and button text */ + display: flex; + align-items: center; + gap: 4px; + + &:hover { + background: transparent; + } + + /* The expand arrow needs to be displayed inside the button to be accessible */ + &::before { + content: ""; + display: inline-block; + width: 10px; + height: 10px; + background-image: url(chrome://devtools/content/debugger/images/arrow.svg); + background-size: contain; + -moz-context-properties: fill; + fill: var(--theme-icon-dimmed-color); + rotate: -90deg; + transition: rotate 180ms var(--animation-curve); + + &:dir(rtl) { + rotate: 90deg; + } + } + + &[aria-expanded="true"]::before { + /* icon should always point to the bottom (default) when expanded, + regardless of the text direction */ + rotate: 0deg !important; + } +} + +.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..3b5d5ae516 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Accordion.js @@ -0,0 +1,89 @@ +/* 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 . */ + +import { cloneElement, Component } from "devtools/client/shared/vendor/react"; +import { + aside, + button, + div, + h2, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +class Accordion extends Component { + static get propTypes() { + return { + items: PropTypes.array.isRequired, + }; + } + + handleHeaderClick(i) { + const item = this.props.items[i]; + const opened = !item.opened; + item.opened = opened; + + if (item.onToggle) { + item.onToggle(opened); + } + + // We force an update because otherwise the accordion + // would not re-render + this.forceUpdate(); + } + + renderContainer = (item, i) => { + const { opened } = item; + const contentElementId = `${item.id}-content`; + + return aside( + { + className: item.className, + key: item.id, + "aria-labelledby": item.id, + role: item.role, + }, + h2( + { + className: "_header", + }, + button( + { + id: item.id, + className: "header-label", + "aria-expanded": `${opened ? "true" : "false"}`, + "aria-controls": opened ? contentElementId : undefined, + onClick: () => this.handleHeaderClick(i), + }, + item.header + ), + item.buttons + ? div( + { + className: "header-buttons", + }, + item.buttons + ) + : null + ), + opened && + div( + { + className: "_content", + id: contentElementId, + }, + cloneElement(item.component, item.componentProps || {}) + ) + ); + }; + render() { + return div( + { + className: "accordion", + }, + this.props.items.map(this.renderContainer) + ); + } +} + +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 . */ + +.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..72571c0f58 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Badge.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 . */ + +import React from "devtools/client/shared/vendor/react"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +class Badge extends React.Component { + constructor(props) { + super(props); + } + + static get propTypes() { + return { + badgeText: PropTypes.node.isRequired, + }; + } + + render() { + return React.createElement( + "span", + { + className: "badge text-white text-center", + }, + this.props.badgeText + ); + } +} + +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 . */ + +.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..40e2cda6c4 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/BracketArrow.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import { div } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +const BracketArrow = ({ orientation, left, top, bottom }) => { + return div({ + className: classnames("bracket-arrow", orientation || "up"), + style: { + left, + top, + bottom, + }, + }); +}; + +BracketArrow.propTypes = { + bottom: PropTypes.number, + left: PropTypes.number, + orientation: PropTypes.string.isRequired, + top: PropTypes.number, +}; + +export default BracketArrow; diff --git a/devtools/client/debugger/src/components/shared/Button/CloseButton.js b/devtools/client/debugger/src/components/shared/Button/CloseButton.js new file mode 100644 index 0000000000..a8f66de60d --- /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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { button } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +import AccessibleImage from "../AccessibleImage"; + +function CloseButton({ handleClick, buttonClass, tooltip }) { + return button( + { + className: buttonClass ? `close-btn ${buttonClass}` : "close-btn", + onClick: handleClick, + title: tooltip, + }, + React.createElement(AccessibleImage, { + className: "close", + }) + ); +} + +CloseButton.propTypes = { + buttonClass: PropTypes.string, + handleClick: PropTypes.func.isRequired, + tooltip: PropTypes.string, +}; + +export default CloseButton; diff --git a/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js new file mode 100644 index 0000000000..4b0b52e186 --- /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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { button } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +import AccessibleImage from "../AccessibleImage"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +export function debugBtn( + onClick, + type, + className, + tooltip, + disabled = false, + ariaPressed = false +) { + return React.createElement( + CommandBarButton, + { + className: classnames(type, className), + disabled: disabled, + key: type, + onClick: onClick, + pressed: ariaPressed, + title: tooltip, + }, + React.createElement(AccessibleImage, { + className: type, + }) + ); +} +const CommandBarButton = props => { + const { children, className, pressed = false, ...rest } = props; + + return button( + { + "aria-pressed": pressed, + className: classnames("command-bar-button", className), + ...rest, + }, + children + ); +}; + +CommandBarButton.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string.isRequired, + pressed: PropTypes.bool, +}; + +export default CommandBarButton; diff --git a/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js new file mode 100644 index 0000000000..ad003552ad --- /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 . */ + +import React, { PureComponent } from "devtools/client/shared/vendor/react"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import AccessibleImage from "../AccessibleImage"; +import { CommandBarButton } from "./index"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +class PaneToggleButton extends PureComponent { + static defaultProps = { + horizontal: false, + position: "start", + }; + + static get propTypes() { + return { + collapsed: PropTypes.bool.isRequired, + handleClick: PropTypes.func.isRequired, + horizontal: PropTypes.bool.isRequired, + position: PropTypes.oneOf(["start", "end"]).isRequired, + }; + } + + label(position, collapsed) { + switch (position) { + case "start": + return L10N.getStr(collapsed ? "expandSources" : "collapseSources"); + case "end": + return L10N.getStr( + collapsed ? "expandBreakpoints" : "collapseBreakpoints" + ); + } + return null; + } + + render() { + const { position, collapsed, horizontal, handleClick } = this.props; + return React.createElement( + CommandBarButton, + { + className: classnames("toggle-button", position, { + collapsed, + vertical: !horizontal, + }), + onClick: () => handleClick(position, !collapsed), + title: this.label(position, collapsed), + }, + React.createElement(AccessibleImage, { + className: collapsed ? "pane-expand" : "pane-collapse", + }) + ); + } +} + +export default PaneToggleButton; diff --git a/devtools/client/debugger/src/components/shared/Button/index.js b/devtools/client/debugger/src/components/shared/Button/index.js new file mode 100644 index 0000000000..df7976ba90 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/index.js @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +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..c2d8df6d38 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css @@ -0,0 +1,35 @@ +/* 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 . */ + +.close-btn { + width: 16px; + height: 16px; + border: 1px solid transparent; + border-radius: 2px; + padding: 1px; + color: var(--theme-icon-color); +} + +.close-btn:hover { + 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..12e53e6fc5 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.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 . */ + +.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; + /* Adjust outline so it's not clipped */ + outline-offset: -3px; +} + +.command-bar-button:disabled { + opacity: 0.6; + cursor: default; +} + +.command-bar-button:not(.disabled):hover, +.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover { + background: var(--theme-toolbar-background-hover); +} + +.theme-dark .command-bar-button:not(.disabled):hover, +.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover { + background: var(--theme-toolbar-hover); +} + +:root.theme-dark .command-bar-button { + color: var(--theme-body-color); +} + +.command-bar-button > * { + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; +} + +/** + * Settings icon and menu + */ +.devtools-button.debugger-settings-menu-button { + border-radius: 0; + margin: 0; + padding: 0; +} + +.devtools-button.debugger-settings-menu-button::before { + background-image: url("chrome://devtools/skin/images/settings.svg"); +} + +.devtools-button.debugger-trace-menu-button::before { + background-image: url(chrome://devtools/content/debugger/images/trace.svg); +} +.devtools-button.debugger-trace-menu-button:is(.active, .pending)::before { + fill: var(--theme-icon-checked-color); +} +.devtools-button.debugger-trace-menu-button.pending::after +{ + content: url("chrome://global/skin/icons/badge-blue.svg"); + width: 14px; + height: 14px; + display: block; + position: absolute; + bottom: -2px; + right: 0; +} 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 . */ + +.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..5e448881d9 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; +import { CloseButton } from "../"; + +describe("CloseButton", () => { + it("renders with tooltip", () => { + const tooltip = "testTooltip"; + const wrapper = shallow( + React.createElement(CloseButton, { + tooltip: tooltip, + handleClick: () => {}, + }) + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles click event", () => { + const handleClickSpy = jest.fn(); + const wrapper = shallow( + React.createElement(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..41537cf8e4 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js @@ -0,0 +1,44 @@ +/* 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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; +import { CommandBarButton, debugBtn } from "../"; + +describe("CommandBarButton", () => { + it("renders", () => { + const wrapper = shallow( + React.createElement(CommandBarButton, { + children: [], + className: "", + }) + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("renders children", () => { + const children = [1, 2, 3, 4]; + const wrapper = shallow( + React.createElement(CommandBarButton, { + children: children, + className: "", + }) + ); + expect(wrapper.find("button").children()).toHaveLength(4); + }); +}); + +describe("debugBtn", () => { + it("renders", () => { + const wrapper = shallow(debugBtn()); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles onClick", () => { + const onClickSpy = jest.fn(); + const wrapper = shallow(debugBtn(onClickSpy)); + wrapper.simulate("click"); + expect(onClickSpy).toHaveBeenCalled(); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js new file mode 100644 index 0000000000..89b548379d --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; +import { PaneToggleButton } from "../"; + +describe("PaneToggleButton", () => { + const handleClickSpy = jest.fn(); + const wrapper = shallow( + React.createElement(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`] = ` + +`; diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap new file mode 100644 index 0000000000..cebcb5892c --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandBarButton renders 1`] = ` + +`; 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`] = ` + + + +`; 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..bb9295b296 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Dropdown.css @@ -0,0 +1,97 @@ +/* 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 . */ + +.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; + outline-offset: -2px; +} + +.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..a47eef9534 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Dropdown.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import { Component } from "devtools/client/shared/vendor/react"; +import { button, div } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +export class Dropdown extends Component { + constructor(props) { + super(props); + this.state = { + dropdownShown: false, + }; + } + + static get propTypes() { + return { + icon: PropTypes.node.isRequired, + panel: PropTypes.node.isRequired, + }; + } + + toggleDropdown = e => { + this.setState(prevState => ({ + dropdownShown: !prevState.dropdownShown, + })); + }; + + renderPanel() { + return div( + { + className: "dropdown", + onClick: this.toggleDropdown, + style: { + display: this.state.dropdownShown ? "block" : "none", + }, + }, + this.props.panel + ); + } + + renderButton() { + return button( + { + className: "dropdown-button", + onClick: this.toggleDropdown, + }, + this.props.icon + ); + } + + 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() + ); + } +} + +export default Dropdown; diff --git a/devtools/client/debugger/src/components/shared/Modal.css b/devtools/client/debugger/src/components/shared/Modal.css new file mode 100644 index 0000000000..2c8f429285 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Modal.css @@ -0,0 +1,47 @@ +/* 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 . */ + +.modal-wrapper { + position: fixed; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 100; +} + +.modal { + display: flex; + flex-direction: column; + /* Place the modal below the sources tab strip */ + margin-block-start: var(--editor-header-height); + width: 80%; + max-height: 80vh; + overflow-y: auto; + background-color: var(--theme-toolbar-background); + box-shadow: 1px 1px 6px 1px var(--popup-shadow-color); + + @media not (prefers-reduced-motion) { + animation: 150ms cubic-bezier(0.07, 0.95, 0, 1) slidein forwards; + } +} + +@keyframes slidein { + from { + transform: translateY(-101%); + } + to { + transform: translateY(0); + } +} + +/* This rule is active when the screen is not narrow */ +@media (min-width: 580px) { + .modal { + width: 50%; + } +} 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..c14732f302 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Modal.js @@ -0,0 +1,45 @@ +/* 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 . */ + +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import React from "devtools/client/shared/vendor/react"; +import { div } from "devtools/client/shared/vendor/react-dom-factories"; +const classnames = require("resource://devtools/client/shared/classnames.js"); + +class Modal extends React.Component { + static get propTypes() { + return { + additionalClass: PropTypes.string, + children: PropTypes.node.isRequired, + handleClose: PropTypes.func.isRequired, + }; + } + + onClick = e => { + e.stopPropagation(); + }; + + render() { + const { additionalClass, children, handleClose } = this.props; + return div( + { + className: "modal-wrapper", + onClick: handleClose, + }, + div( + { + className: classnames("modal", additionalClass), + onClick: this.onClick, + }, + children + ) + ); + } +} + +Modal.contextTypes = { + shortcuts: PropTypes.object, +}; + +export default Modal; 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 . */ + +.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..8748e36418 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/Popover.js @@ -0,0 +1,324 @@ +/* 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 . */ + +import React, { Component } from "devtools/client/shared/vendor/react"; +import { div } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import BracketArrow from "./BracketArrow"; +import SmartGap from "./SmartGap"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +class Popover extends Component { + state = { + coords: { + left: 0, + top: 0, + orientation: "down", + targetMid: { x: 0, y: 0 }, + }, + }; + firstRender = true; + + static defaultProps = { + type: "popover", + }; + + static get propTypes() { + return { + children: PropTypes.node.isRequired, + editorRef: PropTypes.object.isRequired, + mouseout: PropTypes.func.isRequired, + target: PropTypes.object.isRequired, + targetPosition: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, + }; + } + + componentDidMount() { + const { type } = this.props; + this.gapHeight = this.$gap.getBoundingClientRect().height; + const coords = + type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords(); + + if (coords) { + this.setState({ coords }); + } + + this.firstRender = false; + this.startTimer(); + } + + componentDidUpdate(prevProps) { + // We have to update `coords` when the Popover type changes + if ( + prevProps.type != this.props.type || + prevProps.target !== this.props.target + ) { + const coords = + this.props.type == "popover" + ? this.getPopoverCoords() + : this.getTooltipCoords(); + + if (coords) { + this.setState({ coords }); + } + } + } + + componentWillUnmount() { + if (this.timerId) { + clearTimeout(this.timerId); + } + } + + startTimer() { + this.timerId = setTimeout(this.onTimeout, 0); + } + + onTimeout = () => { + const isHoveredOnGap = this.$gap && this.$gap.matches(":hover"); + const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover"); + const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover"); + const isHoveredOnTarget = this.props.target.matches(":hover"); + + if (isHoveredOnGap) { + if (!this.wasOnGap) { + this.wasOnGap = true; + this.timerId = setTimeout(this.onTimeout, 200); + return; + } + this.props.mouseout(); + return; + } + + // Don't clear the current preview if mouse is hovered on + // the current preview's token (target) or the popup element + if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) { + this.wasOnGap = false; + this.timerId = setTimeout(this.onTimeout, 0); + return; + } + + this.props.mouseout(); + }; + + calculateLeft(target, editor, popover, orientation) { + const estimatedLeft = target.left; + const estimatedRight = estimatedLeft + popover.width; + const isOverflowingRight = estimatedRight > editor.right; + if (orientation === "right") { + return target.left + target.width; + } + if (isOverflowingRight) { + const adjustedLeft = editor.right - popover.width - 8; + return adjustedLeft; + } + return estimatedLeft; + } + + calculateTopForRightOrientation = (target, editor, popover) => { + if (popover.height <= editor.height) { + const rightOrientationTop = target.top - popover.height / 2; + if (rightOrientationTop < editor.top) { + return editor.top - target.height; + } + const rightOrientationBottom = rightOrientationTop + popover.height; + if (rightOrientationBottom > editor.bottom) { + return editor.bottom + target.height - popover.height + this.gapHeight; + } + return rightOrientationTop; + } + return editor.top - target.height; + }; + + calculateOrientation(target, editor, popover) { + const estimatedBottom = target.bottom + popover.height; + if (editor.bottom > estimatedBottom) { + return "down"; + } + const upOrientationTop = target.top - popover.height; + if (upOrientationTop > editor.top) { + return "up"; + } + + return "right"; + } + + calculateTop = (target, editor, popover, orientation) => { + if (orientation === "down") { + return target.bottom; + } + if (orientation === "up") { + return target.top - popover.height; + } + + return this.calculateTopForRightOrientation(target, editor, popover); + }; + + getPopoverCoords() { + if (!this.$popover || !this.props.editorRef) { + return null; + } + + const popover = this.$popover; + const editor = this.props.editorRef; + const popoverRect = popover.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + const targetRect = this.props.targetPosition; + const orientation = this.calculateOrientation( + targetRect, + editorRect, + popoverRect + ); + const top = this.calculateTop( + targetRect, + editorRect, + popoverRect, + orientation + ); + const popoverLeft = this.calculateLeft( + targetRect, + editorRect, + popoverRect, + orientation + ); + let targetMid; + if (orientation === "right") { + targetMid = { + x: -14, + y: targetRect.top - top - 2, + }; + } else { + targetMid = { + x: targetRect.left - popoverLeft + targetRect.width / 2 - 8, + y: 0, + }; + } + + return { + left: popoverLeft, + top, + orientation, + targetMid, + }; + } + + getTooltipCoords() { + if (!this.$tooltip || !this.props.editorRef) { + return null; + } + const tooltip = this.$tooltip; + const editor = this.props.editorRef; + const tooltipRect = tooltip.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + const targetRect = this.props.targetPosition; + const left = this.calculateLeft(targetRect, editorRect, tooltipRect); + const enoughRoomForTooltipAbove = + targetRect.top - editorRect.top > tooltipRect.height; + const top = enoughRoomForTooltipAbove + ? targetRect.top - tooltipRect.height + : targetRect.bottom; + + return { + left, + top, + orientation: enoughRoomForTooltipAbove ? "up" : "down", + targetMid: { x: 0, y: 0 }, + }; + } + + getChildren() { + const { children } = this.props; + const { coords } = this.state; + const gap = this.getGap(); + + return coords.orientation === "up" ? [children, gap] : [gap, children]; + } + + getGap() { + if (this.firstRender) { + return div({ + className: "gap", + key: "gap", + ref: a => (this.$gap = a), + }); + } + + return div( + { + className: "gap", + key: "gap", + ref: a => (this.$gap = a), + }, + React.createElement(SmartGap, { + token: this.props.target, + preview: this.$tooltip || this.$popover, + type: this.props.type, + gapHeight: this.gapHeight, + coords: this.state.coords, + offset: this.$gap.getBoundingClientRect().left, + }) + ); + } + + getPopoverArrow(orientation, left, top) { + let arrowProps = {}; + + if (orientation === "up") { + arrowProps = { orientation: "down", bottom: 10, left }; + } else if (orientation === "down") { + arrowProps = { orientation: "up", top: -2, left }; + } else { + arrowProps = { orientation: "left", top, left: -4 }; + } + return React.createElement(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() + ); + } + + renderTooltip() { + const { top, left, orientation } = this.state.coords; + return div( + { + className: `tooltip orientation-${orientation}`, + style: { + top, + left, + }, + ref: c => (this.$tooltip = c), + }, + this.getChildren() + ); + } + + 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 . */ + +.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..1a6d164cdf --- /dev/null +++ b/devtools/client/debugger/src/components/shared/PreviewFunction.js @@ -0,0 +1,108 @@ +/* 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 . */ + +import { Component } from "devtools/client/shared/vendor/react"; +import { + span, + button, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +import { formatDisplayName } from "../../utils/pause/frames/index"; + +const IGNORED_SOURCE_URLS = ["debugger eval code"]; + +export default class PreviewFunction extends Component { + static get propTypes() { + return { + func: PropTypes.object.isRequired, + }; + } + + renderFunctionName(func) { + const { l10n } = this.context; + const name = formatDisplayName(func, undefined, l10n); + return span( + { + className: "function-name", + }, + name + ); + } + + renderParams(func) { + const { parameterNames = [] } = func; + + return parameterNames + .filter(Boolean) + .map((param, i, arr) => { + const elements = [ + span( + { + className: "param", + key: param, + }, + param + ), + ]; + // if this isn't the last param, add a comma + if (i !== arr.length - 1) { + elements.push( + span( + { + className: "delimiter", + key: i, + }, + ", " + ) + ); + } + return elements; + }) + .flat(); + } + + jumpToDefinitionButton(func) { + const { location } = func; + + if (!location?.url || IGNORED_SOURCE_URLS.includes(location.url)) { + return null; + } + + const lastIndex = location.url.lastIndexOf("/"); + return button({ + className: "jump-definition", + draggable: "false", + title: `${location.url.slice(lastIndex + 1)}:${location.line}`, + }); + } + + render() { + const { func } = this.props; + return span( + { + className: "function-signature", + }, + this.renderFunctionName(func), + span( + { + className: "paren", + }, + "(" + ), + this.renderParams(func), + span( + { + className: "paren", + }, + ")" + ), + this.jumpToDefinitionButton(func) + ); + } +} + +PreviewFunction.contextTypes = { + l10n: PropTypes.object, +}; diff --git a/devtools/client/debugger/src/components/shared/ResultList.css b/devtools/client/debugger/src/components/shared/ResultList.css new file mode 100644 index 0000000000..037c3497d3 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ResultList.css @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +.result-list { + list-style: none; + margin: 0px; + padding: 0px; + overflow: auto; + width: 100%; + background: var(--theme-body-background); +} + +.result-list * { + user-select: none; +} + +.result-list li { + color: var(--theme-body-color); + padding: 4px 8px; + display: flex; +} + +.result-list.big li { + flex-direction: row; + align-items: center; + padding: 6px 8px; + font-size: 12px; + line-height: 16px; +} + +.result-list.small li { + justify-content: space-between; +} + +.result-list li:hover { + background: var(--theme-tab-toolbar-background); +} + +.theme-dark .result-list li:hover { + background: var(--grey-70); +} + +.result-list li.selected { + background: var(--theme-accordion-header-background); +} + +.result-list.small li.selected { + background-color: var(--theme-selection-background); + color: white; +} + +.result-list li .result-item-icon { + background-color: var(--theme-icon-dimmed-color); +} + +.result-list li .icon { + align-self: center; + margin-inline-end: 14px; + margin-inline-start: 4px; +} + +.result-list .result-item-icon { + display: block; +} + +.result-list .selected .result-item-icon { + background-color: var(--theme-selection-color); +} + +.result-list li .title { + word-break: break-all; + text-overflow: ellipsis; + white-space: nowrap; + + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-90); +} + +.theme-dark .result-list li .title { + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-30); +} + +.result-list li.selected .title { + color: white; +} + +.result-list.big li.selected { + background-color: var(--theme-selection-background); + color: white; +} + +.result-list.big li.selected .subtitle { + color: white; +} + +.result-list.big li.selected .subtitle .highlight { + color: white; + font-weight: bold; +} + +.result-list.big li .subtitle { + word-break: break-all; + /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/ + color: var(--grey-40); + margin-left: 15px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.theme-dark .result-list.big li.selected .subtitle { + color: white; +} + +.theme-dark .result-list.big li .subtitle { + color: var(--theme-text-color-inactive); +} + +.search-bar .result-list li.selected .subtitle { + color: white; +} + +.search-bar .result-list { + border-bottom: 1px solid var(--theme-splitter-color); +} + +.theme-dark .result-list { + background-color: var(--theme-body-background); +} diff --git a/devtools/client/debugger/src/components/shared/ResultList.js b/devtools/client/debugger/src/components/shared/ResultList.js new file mode 100644 index 0000000000..6b29de51f4 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/ResultList.js @@ -0,0 +1,102 @@ +/* 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 . */ + +import React, { Component } from "devtools/client/shared/vendor/react"; +import { li, div, ul } from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +import AccessibleImage from "./AccessibleImage"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +import { scrollList } from "../../utils/result-list"; + +export default class ResultList extends Component { + static defaultProps = { + size: "small", + role: "listbox", + }; + + static get propTypes() { + return { + items: PropTypes.array.isRequired, + role: PropTypes.oneOf(["listbox"]), + selectItem: PropTypes.func.isRequired, + selected: PropTypes.number.isRequired, + size: PropTypes.oneOf(["big", "small"]), + }; + } + + constructor(props) { + super(props); + this.ref = React.createRef(); + } + + componentDidUpdate() { + if (this.ref.current.childNodes) { + scrollList(this.ref.current.childNodes, this.props.selected); + } + } + + renderListItem = (item, index) => { + if (item.value === "/" && item.title === "") { + item.title = "(index)"; + } + + const { selectItem, selected } = this.props; + const props = { + onClick: event => selectItem(event, item, index), + key: `${item.id}${item.value}${index}`, + 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", + }, + React.createElement(AccessibleImage, { + className: item.icon, + }) + ), + div( + { + id: `${item.id}-title`, + className: "title", + }, + item.title + ), + item.subtitle != item.title + ? div( + { + id: `${item.id}-subtitle`, + className: "subtitle", + }, + item.subtitle + ) + : null + ); + }; + render() { + const { size, items, role } = this.props; + return ul( + { + ref: this.ref, + className: classnames("result-list", size), + id: "result-list", + role: role, + "aria-live": "polite", + }, + items.map(this.renderListItem) + ); + } +} 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..4a5ee85ed3 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SearchInput.css @@ -0,0 +1,223 @@ +/* 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 . */ + +.search-outline { + border: 1px solid var(--theme-toolbar-background); + border-bottom: 1px solid var(--theme-splitter-color); + transition: border-color 200ms ease-in-out; + display: flex; + flex-direction: column; +} + +.search-field { + position: relative; + display: flex; + align-items: center; + flex-shrink: 0; + min-height: 24px; + width: 100%; +} + +.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; + outline-offset: -1px; + + &:focus-visible { + /* Don't show the box-shadow focus indicator, only keep the outline, otherwise the + shadow overlap the first item in the result list */ + box-shadow: none; + } +} + +.exclude-patterns-field { + position: relative; + display: flex; + align-items: flex-start; + flex-direction: column; + flex-shrink: 0; + min-height: 24px; + width: 100%; + border-top: 1px solid var(--theme-splitter-color); + margin-top: 1px; + outline-offset: -1px; +} + +.exclude-patterns-field label { + padding-inline-start: 8px; + padding-top: 5px; + padding-bottom: 3px; + align-self: stretch; + background-color: var(--theme-accordion-header-background); + font-size: 12px; +} + +.exclude-patterns-field input { + align-self: stretch; + height: 24px; + border: none; + padding-top: 14px; + padding-bottom: 14px; + padding-inline-start: 10px; + line-height: 16px; + font-family: inherit; + font-size: inherit; + color: var(--theme-body-color); + background-color: transparent; + border-top: 1px solid var(--theme-splitter-color); + min-height: 24px; + outline-offset: -1px; +} + +.exclude-patterns-field input::placeholder { + color: var(--theme-text-color-alt); + opacity: 1; +} + +.search-field.big input { + height: 40px; + padding-top: 10px; + padding-bottom: 10px; + padding-inline-start: 40px; + font-size: 14px; + line-height: 20px; +} + +.search-field 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; + outline-offset: -2px; +} + +.search-field .search-nav-buttons .nav-btn:hover { + background-color: var(--theme-toolbar-background-hover); +} + +.search-field .close-btn { + margin-inline-end: 4px; +} + +.search-field.big .close-btn { + margin-inline-end: 8px; +} + +.search-field .close-btn::-moz-focus-inner { + border: none; +} + +.search-buttons-bar .pipe-divider { + flex: none; + align-self: stretch; + width: 1px; + vertical-align: middle; + margin: 4px; + background-color: var(--theme-splitter-color); +} + +.search-buttons-bar * { + user-select: none; +} + +.search-buttons-bar { + display: flex; + flex-shrink: 0; + justify-content: flex-end; + align-items: center; + padding: 0; +} + +.search-buttons-bar .search-type-toggles { + display: flex; + align-items: center; + max-width: 68%; +} + +.search-buttons-bar .search-type-name { + margin: 0 4px; + border: none; + background: transparent; + color: var(--theme-comment); +} + +.search-buttons-bar .search-type-toggles .search-type-btn.active { + color: var(--theme-selection-background); +} + +.theme-dark .search-buttons-bar .search-type-toggles .search-type-btn.active { + color: white; +} + +.search-buttons-bar .close-btn { + margin-inline-end: 3px; +} diff --git a/devtools/client/debugger/src/components/shared/SearchInput.js b/devtools/client/debugger/src/components/shared/SearchInput.js new file mode 100644 index 0000000000..18f6ffbebb --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SearchInput.js @@ -0,0 +1,362 @@ +/* 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 . */ + +import React, { Component } from "devtools/client/shared/vendor/react"; +import { + button, + div, + label, + input, + span, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import { connect } from "devtools/client/shared/vendor/react-redux"; +import { CloseButton } from "./Button/index"; + +import AccessibleImage from "./AccessibleImage"; +import actions from "../../actions/index"; +import { getSearchOptions } from "../../selectors/index"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); +const SearchModifiers = require("resource://devtools/client/shared/components/SearchModifiers.js"); + +const arrowBtn = (onClick, type, className, tooltip) => { + const props = { + className, + key: type, + onClick, + title: tooltip, + type, + }; + return button( + props, + React.createElement(AccessibleImage, { + className: type, + }) + ); +}; + +export class SearchInput extends Component { + static defaultProps = { + expanded: false, + hasPrefix: false, + selectedItemId: "", + size: "", + showClose: true, + }; + + constructor(props) { + super(props); + this.state = { + history: [], + excludePatterns: props.searchOptions.excludePatterns, + }; + } + + static get propTypes() { + return { + count: PropTypes.number.isRequired, + expanded: PropTypes.bool.isRequired, + handleClose: PropTypes.func, + handleNext: PropTypes.func, + handlePrev: PropTypes.func, + hasPrefix: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + onBlur: PropTypes.func, + onChange: PropTypes.func, + onFocus: PropTypes.func, + onHistoryScroll: PropTypes.func, + onKeyDown: PropTypes.func, + onKeyUp: PropTypes.func, + placeholder: PropTypes.string, + query: PropTypes.string, + selectedItemId: PropTypes.string, + shouldFocus: PropTypes.bool, + showClose: PropTypes.bool.isRequired, + showExcludePatterns: PropTypes.bool.isRequired, + excludePatternsLabel: PropTypes.string, + excludePatternsPlaceholder: PropTypes.string, + showErrorEmoji: PropTypes.bool.isRequired, + size: PropTypes.string, + summaryMsg: PropTypes.string, + searchKey: PropTypes.string.isRequired, + searchOptions: PropTypes.object, + setSearchOptions: PropTypes.func, + showSearchModifiers: PropTypes.bool.isRequired, + onToggleSearchModifier: PropTypes.func, + }; + } + + componentDidMount() { + this.setFocus(); + } + + componentDidUpdate(prevProps) { + if (this.props.shouldFocus && !prevProps.shouldFocus) { + this.setFocus(); + } + } + + setFocus() { + if (this.$input) { + const _input = this.$input; + _input.focus(); + + if (!_input.value) { + return; + } + + // omit prefix @:# from being selected + const selectStartPos = this.props.hasPrefix ? 1 : 0; + _input.setSelectionRange(selectStartPos, _input.value.length + 1); + } + } + + renderArrowButtons() { + const { handleNext, handlePrev } = this.props; + + return [ + arrowBtn( + handlePrev, + "arrow-up", + classnames("nav-btn", "prev"), + L10N.getFormatStr("editor.searchResults.prevResult") + ), + arrowBtn( + handleNext, + "arrow-down", + classnames("nav-btn", "next"), + L10N.getFormatStr("editor.searchResults.nextResult") + ), + ]; + } + + onFocus = e => { + const { onFocus } = this.props; + + if (onFocus) { + onFocus(e); + } + }; + + onBlur = e => { + const { onBlur } = this.props; + + if (onBlur) { + onBlur(e); + } + }; + + onKeyDown = e => { + const { onHistoryScroll, onKeyDown } = this.props; + if (!onHistoryScroll) { + onKeyDown(e); + return; + } + + const inputValue = e.target.value; + const { history } = this.state; + const currentHistoryIndex = history.indexOf(inputValue); + + if (e.key === "Enter") { + this.saveEnteredTerm(inputValue); + onKeyDown(e); + return; + } + + if (e.key === "ArrowUp") { + const previous = + currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1; + const previousInHistory = history[previous]; + if (previousInHistory) { + e.preventDefault(); + onHistoryScroll(previousInHistory); + } + return; + } + + if (e.key === "ArrowDown") { + const next = currentHistoryIndex + 1; + const nextInHistory = history[next]; + if (nextInHistory) { + onHistoryScroll(nextInHistory); + } + } + }; + + onExcludeKeyDown = e => { + if (e.key === "Enter") { + this.props.setSearchOptions(this.props.searchKey, { + excludePatterns: this.state.excludePatterns, + }); + this.props.onKeyDown(e); + } + }; + + saveEnteredTerm(query) { + const { history } = this.state; + const previousIndex = history.indexOf(query); + if (previousIndex !== -1) { + history.splice(previousIndex, 1); + } + history.push(query); + this.setState({ history }); + } + + renderSummaryMsg() { + const { summaryMsg } = this.props; + + if (!summaryMsg) { + return null; + } + return div( + { + className: "search-field-summary", + }, + summaryMsg + ); + } + + renderSpinner() { + const { isLoading } = this.props; + if (!isLoading) { + return null; + } + return React.createElement(AccessibleImage, { + className: "loader spin", + }); + } + + renderNav() { + const { count, handleNext, handlePrev } = this.props; + if ((!handleNext && !handlePrev) || !count || count == 1) { + return null; + } + return div( + { + className: "search-nav-buttons", + }, + this.renderArrowButtons() + ); + } + + renderSearchModifiers() { + if (!this.props.showSearchModifiers) { + return null; + } + return React.createElement(SearchModifiers, { + modifiers: this.props.searchOptions, + onToggleSearchModifier: updatedOptions => { + this.props.setSearchOptions(this.props.searchKey, updatedOptions); + this.props.onToggleSearchModifier(); + }, + }); + } + + renderExcludePatterns() { + if (!this.props.showExcludePatterns) { + return null; + } + return div( + { + className: classnames("exclude-patterns-field", this.props.size), + }, + label(null, this.props.excludePatternsLabel), + input({ + placeholder: this.props.excludePatternsPlaceholder, + value: this.state.excludePatterns, + onKeyDown: this.onExcludeKeyDown, + onChange: e => + this.setState({ + excludePatterns: e.target.value, + }), + }) + ); + } + + renderClose() { + if (!this.props.showClose) { + return null; + } + return React.createElement( + React.Fragment, + null, + span({ + className: "pipe-divider", + }), + React.createElement(CloseButton, { + handleClick: this.props.handleClose, + buttonClass: this.props.size, + }) + ); + } + + render() { + const { + expanded, + onChange, + onKeyUp, + placeholder, + query, + selectedItemId, + showErrorEmoji, + size, + } = this.props; + + const inputProps = { + className: classnames({ + empty: showErrorEmoji, + }), + onChange, + onKeyDown: e => this.onKeyDown(e), + onKeyUp, + onFocus: e => this.onFocus(e), + onBlur: e => this.onBlur(e), + "aria-autocomplete": "list", + "aria-controls": "result-list", + "aria-activedescendant": + expanded && selectedItemId ? `${selectedItemId}-title` : "", + placeholder, + value: query, + spellCheck: false, + ref: c => (this.$input = c), + }; + return div( + { + className: "search-outline", + }, + div( + { + className: classnames("search-field", size), + role: "combobox", + "aria-haspopup": "listbox", + "aria-owns": "result-list", + "aria-expanded": expanded, + }, + React.createElement(AccessibleImage, { + className: "search", + }), + input(inputProps), + this.renderSpinner(), + this.renderSummaryMsg(), + this.renderNav(), + div( + { + className: "search-buttons-bar", + }, + this.renderSearchModifiers(), + this.renderClose() + ) + ), + this.renderExcludePatterns() + ); + } +} +const mapStateToProps = (state, props) => ({ + searchOptions: getSearchOptions(state, props.searchKey), +}); + +export default connect(mapStateToProps, { + setSearchOptions: actions.setSearchOptions, +})(SearchInput); diff --git a/devtools/client/debugger/src/components/shared/SmartGap.js b/devtools/client/debugger/src/components/shared/SmartGap.js new file mode 100644 index 0000000000..d76d018987 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SmartGap.js @@ -0,0 +1,170 @@ +/* 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 . */ + +import { + svg, + polygon, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +function shorten(coordinates) { + // In cases where the token is wider than the preview, the smartGap + // gets distorted. This shortens the coordinate array so that the smartGap + // is only touching 2 corners of the token (instead of all 4 corners) + coordinates.splice(0, 2); + coordinates.splice(4, 2); + return coordinates; +} + +function getSmartGapCoordinates( + preview, + token, + offset, + orientation, + gapHeight, + coords +) { + if (orientation === "up") { + const coordinates = [ + token.left - coords.left + offset, + token.top + token.height - (coords.top + preview.height) + gapHeight, + 0, + 0, + preview.width + offset, + 0, + token.left + token.width - coords.left + offset, + token.top + token.height - (coords.top + preview.height) + gapHeight, + token.left + token.width - coords.left + offset, + token.top - (coords.top + preview.height) + gapHeight, + token.left - coords.left + offset, + token.top - (coords.top + preview.height) + gapHeight, + ]; + return preview.width > token.width ? coordinates : shorten(coordinates); + } + if (orientation === "down") { + const coordinates = [ + token.left + token.width - (coords.left + preview.top) + offset, + 0, + preview.width + offset, + coords.top - token.top + gapHeight, + 0, + coords.top - token.top + gapHeight, + token.left - (coords.left + preview.top) + offset, + 0, + token.left - (coords.left + preview.top) + offset, + token.height, + token.left + token.width - (coords.left + preview.top) + offset, + token.height, + ]; + return preview.width > token.width ? coordinates : shorten(coordinates); + } + return [ + 0, + token.top - coords.top, + gapHeight + token.width, + 0, + gapHeight + token.width, + preview.height - gapHeight, + 0, + token.top + token.height - coords.top, + token.width, + token.top + token.height - coords.top, + token.width, + token.top - coords.top, + ]; +} + +function getSmartGapDimensions( + previewRect, + tokenRect, + offset, + orientation, + gapHeight, + coords +) { + if (orientation === "up") { + return { + height: + tokenRect.top + + tokenRect.height - + coords.top - + previewRect.height + + gapHeight, + width: Math.max(previewRect.width, tokenRect.width) + offset, + }; + } + if (orientation === "down") { + return { + height: coords.top - tokenRect.top + gapHeight, + width: Math.max(previewRect.width, tokenRect.width) + offset, + }; + } + return { + height: previewRect.height - gapHeight, + width: coords.left - tokenRect.left + gapHeight, + }; +} + +export default function SmartGap({ + token, + preview, + type, + gapHeight, + coords, + offset, +}) { + const tokenRect = token.getBoundingClientRect(); + const previewRect = preview.getBoundingClientRect(); + const { orientation } = coords; + let optionalMarginLeft, optionalMarginTop; + + if (orientation === "down") { + optionalMarginTop = -tokenRect.height; + } else if (orientation === "right") { + optionalMarginLeft = -tokenRect.width; + } + + const { height, width } = getSmartGapDimensions( + previewRect, + tokenRect, + -offset, + orientation, + gapHeight, + coords + ); + const coordinates = getSmartGapCoordinates( + previewRect, + tokenRect, + -offset, + orientation, + gapHeight, + coords + ); + return svg( + { + version: "1.1", + xmlns: "http://www.w3.org/2000/svg", + style: { + height, + width, + position: "absolute", + marginLeft: optionalMarginLeft, + marginTop: optionalMarginTop, + }, + }, + polygon({ + points: coordinates, + fill: "transparent", + }) + ); +} + +SmartGap.propTypes = { + coords: PropTypes.object.isRequired, + gapHeight: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + preview: PropTypes.object.isRequired, + token: PropTypes.object.isRequired, + type: PropTypes.oneOf(["popover", "tooltip"]).isRequired, +}; diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.css b/devtools/client/debugger/src/components/shared/SourceIcon.css new file mode 100644 index 0000000000..0b9bf3e79e --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SourceIcon.css @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +/** + * Variant of AccessibleImage used in sources list and tabs. + * Define the different source type / framework / library icons here. + */ + +.source-icon { + margin-inline-end: 4px; +} + +/* Icons for frameworks and libs */ + +.img.aframe { + background-image: url(chrome://devtools/content/debugger/images/sources/aframe.svg); + background-color: transparent !important; +} + +.img.angular { + background-image: url(chrome://devtools/content/debugger/images/sources/angular.svg); + background-color: transparent !important; +} + +.img.babel { + mask-image: url(chrome://devtools/content/debugger/images/sources/babel.svg); +} + +.img.backbone { + mask-image: url(chrome://devtools/content/debugger/images/sources/backbone.svg); +} + +.img.choo { + background-image: url(chrome://devtools/content/debugger/images/sources/choo.svg); + background-color: transparent !important; +} + +.img.coffeescript { + background-image: url(chrome://devtools/content/debugger/images/sources/coffeescript.svg); + background-color: transparent !important; + fill: var(--theme-icon-color); + -moz-context-properties: fill; +} + +.img.dojo { + background-image: url(chrome://devtools/content/debugger/images/sources/dojo.svg); + background-color: transparent !important; +} + +.img.ember { + background-image: url(chrome://devtools/content/debugger/images/sources/ember.svg); + background-color: transparent !important; +} + +.img.express { + mask-image: url(chrome://devtools/content/debugger/images/sources/express.svg); +} + +.img.extension { + mask-image: url(chrome://devtools/content/debugger/images/sources/extension.svg); +} + +.img.immutable { + mask-image: url(chrome://devtools/content/debugger/images/sources/immutable.svg); +} + +.img.javascript { + background-image: url(chrome://devtools/content/debugger/images/sources/javascript.svg); + background-size: 14px 14px; + background-color: transparent !important; + fill: var(--theme-icon-color); + -moz-context-properties: fill; +} + +.img.override::after { + content: ""; + display: block; + height: 5px; + width: 5px; + background-color: var(--purple-30); + border-radius: 100%; + outline: 1px solid var(--theme-sidebar-background); + translate: 12px 10px; +} + +.node.focused .img.override::after { + outline-color: var(--theme-selection-background); +} + +.img.jquery { + mask-image: url(chrome://devtools/content/debugger/images/sources/jquery.svg); +} + +.img.lodash { + mask-image: url(chrome://devtools/content/debugger/images/sources/lodash.svg); +} + +.img.marko { + background-image: url(chrome://devtools/content/debugger/images/sources/marko.svg); + background-color: transparent !important; +} + +.img.mobx { + background-image: url(chrome://devtools/content/debugger/images/sources/mobx.svg); + background-color: transparent !important; +} + +.img.nextjs { + background-image: url(chrome://devtools/content/debugger/images/sources/nextjs.svg); + background-color: transparent !important; +} + +.img.node { + background-image: url(chrome://devtools/content/debugger/images/sources/node.svg); + background-color: transparent !important; +} + +.img.nuxtjs { + background-image: url(chrome://devtools/content/debugger/images/sources/nuxtjs.svg); + background-color: transparent !important; +} + +.img.preact { + background-image: url(chrome://devtools/content/debugger/images/sources/preact.svg); + background-color: transparent !important; +} + +.img.pug { + background-image: url(chrome://devtools/content/debugger/images/sources/pug.svg); + background-color: transparent !important; +} + +.img.react { + background-image: url(chrome://devtools/content/debugger/images/sources/react.svg); + background-color: transparent !important; + fill: var(--theme-highlight-bluegrey); + -moz-context-properties: fill; +} + +.img.redux { + mask-image: url(chrome://devtools/content/debugger/images/sources/redux.svg); +} + +.img.rxjs { + background-image: url(chrome://devtools/content/debugger/images/sources/rxjs.svg); + background-color: transparent !important; +} + +.img.sencha-extjs { + background-image: url(chrome://devtools/content/debugger/images/sources/sencha-extjs.svg); + background-color: transparent !important; +} + +.img.typescript { + background-image: url(chrome://devtools/content/debugger/images/sources/typescript.svg); + background-color: transparent !important; + fill: var(--theme-icon-color); + -moz-context-properties: fill; +} + +.img.underscore { + mask-image: url(chrome://devtools/content/debugger/images/sources/underscore.svg); +} + +/* We use both 'Vue' and 'VueJS' when identifying frameworks */ +.img.vue, +.img.vuejs { + background-image: url(chrome://devtools/content/debugger/images/sources/vuejs.svg); + background-color: transparent !important; +} + +.img.webpack { + background-image: url(chrome://devtools/content/debugger/images/sources/webpack.svg); + background-color: transparent !important; +} diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.js b/devtools/client/debugger/src/components/shared/SourceIcon.js new file mode 100644 index 0000000000..b2a7486bd6 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/SourceIcon.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React, { PureComponent } from "devtools/client/shared/vendor/react"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; + +import { connect } from "devtools/client/shared/vendor/react-redux"; + +import AccessibleImage from "./AccessibleImage"; + +import { getSourceClassnames } from "../../utils/source"; +import { + getSymbols, + isSourceBlackBoxed, + hasPrettyTab, +} from "../../selectors/index"; + +class SourceIcon extends PureComponent { + static get propTypes() { + return { + modifier: PropTypes.func.isRequired, + location: PropTypes.object.isRequired, + iconClass: PropTypes.string, + forTab: PropTypes.bool, + }; + } + + render() { + const { modifier } = this.props; + let { iconClass } = this.props; + + if (modifier) { + const modified = modifier(iconClass); + if (!modified) { + return null; + } + iconClass = modified; + } + return React.createElement(AccessibleImage, { + className: `source-icon ${iconClass}`, + }); + } +} + +export default connect((state, props) => { + const { forTab, location } = props; + // BreakpointHeading sometimes spawn locations without source actor for generated sources + // which disallows fetching symbols. In such race condition return the default icon. + // (this reproduces when running browser_dbg-breakpoints-popup.js) + if (!location.source.isOriginal && !location.sourceActor) { + return "file"; + } + const symbols = getSymbols(state, location); + const isBlackBoxed = isSourceBlackBoxed(state, location.source); + // For the tab icon, we don't want to show the pretty icon for the non-pretty tab + const hasMatchingPrettyTab = !forTab && hasPrettyTab(state, location.source); + + // This is the key function that will compute the icon type, + // In addition to the "modifier" implemented by each callsite. + const iconClass = getSourceClassnames( + location.source, + symbols, + isBlackBoxed, + hasMatchingPrettyTab + ); + + return { + iconClass, + }; +})(SourceIcon); diff --git a/devtools/client/debugger/src/components/shared/menu.css b/devtools/client/debugger/src/components/shared/menu.css new file mode 100644 index 0000000000..37dfbc2e8f --- /dev/null +++ b/devtools/client/debugger/src/components/shared/menu.css @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +menupopup { + position: fixed; + z-index: 10000; + border: 1px solid #cccccc; + padding: 5px 0; + background: #f2f2f2; + border-radius: 5px; + color: #585858; + box-shadow: 0 0 4px 0 rgba(190, 190, 190, 0.8); + min-width: 130px; +} + +menuitem { + display: block; + padding: 0 20px; + line-height: 20px; + font-weight: 500; + font-size: 13px; + user-select: none; +} + +menuitem:hover { + background: #3780fb; + color: white; +} + +menuitem[disabled="true"] { + color: #cccccc; +} + +menuitem[disabled="true"]:hover { + background-color: transparent; + cursor: default; +} + +menuseparator { + border-bottom: 1px solid #cacdd3; + width: 100%; + height: 5px; + display: block; + margin-bottom: 5px; +} + +#contextmenu-mask.show { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} diff --git a/devtools/client/debugger/src/components/shared/moz.build b/devtools/client/debugger/src/components/shared/moz.build new file mode 100644 index 0000000000..b30ea0ab4f --- /dev/null +++ b/devtools/client/debugger/src/components/shared/moz.build @@ -0,0 +1,23 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "Button", +] + +CompiledModules( + "AccessibleImage.js", + "Accordion.js", + "Badge.js", + "BracketArrow.js", + "Dropdown.js", + "Modal.js", + "Popover.js", + "PreviewFunction.js", + "ResultList.js", + "SearchInput.js", + "SourceIcon.js", + "SmartGap.js", +) diff --git a/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js new file mode 100644 index 0000000000..cbe5ab12bf --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js @@ -0,0 +1,47 @@ +/* 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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; + +import Accordion from "../Accordion"; + +describe("Accordion", () => { + const testItems = [ + { + header: "Test Accordion Item 1", + id: "accordion-item-1", + className: "accordion-item-1", + component: React.createElement("div", null), + opened: false, + onToggle: jest.fn(), + }, + { + header: "Test Accordion Item 2", + id: "accordion-item-2", + className: "accordion-item-2", + component: React.createElement("div", null), + buttons: React.createElement("button", null), + opened: false, + onToggle: jest.fn(), + }, + { + header: "Test Accordion Item 3", + id: "accordion-item-3", + className: "accordion-item-3", + component: React.createElement("div", null), + opened: true, + onToggle: jest.fn(), + }, + ]; + const wrapper = shallow( + React.createElement(Accordion, { + items: testItems, + }) + ); + it("basic render", () => expect(wrapper).toMatchSnapshot()); + wrapper.find(".accordion-item-1 button").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..a19b35a7c2 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; + +import Badge from "../Badge"; + +describe("Badge", () => { + it("render", () => + expect( + shallow( + React.createElement(Badge, { + badgeText: 3, + }) + ) + ).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..4ce9a5b5ce --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; + +import BracketArrow from "../BracketArrow"; + +describe("BracketArrow", () => { + const wrapper = shallow( + React.createElement(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..9b001ba9e5 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Dropdown.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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; + +import Dropdown from "../Dropdown"; + +describe("Dropdown", () => { + const wrapper = shallow( + React.createElement(Dropdown, { + panel: React.createElement("div", null), + icon: "✅", + }) + ); + it("render", () => expect(wrapper).toMatchSnapshot()); + wrapper.find(".dropdown").simulate("click"); + it("handle toggleDropdown", () => + expect(wrapper.state().dropdownShown).toEqual(true)); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/Modal.spec.js b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js new file mode 100644 index 0000000000..58c38502e7 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; + +import Modal from "../Modal"; + +describe("Modal", () => { + it("renders", () => { + const wrapper = shallow( + React.createElement(Modal, { + handleClose: () => {}, + }) + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("handles close modal click", () => { + const handleCloseSpy = jest.fn(); + const wrapper = shallow( + React.createElement(Modal, { + handleClose: handleCloseSpy, + }) + ); + wrapper.find(".modal-wrapper").simulate("click"); + expect(handleCloseSpy).toHaveBeenCalled(); + }); + + it("renders children", () => { + const wrapper = shallow( + React.createElement( + Modal, + { + handleClose: () => {}, + }, + React.createElement("div", { + className: "aChild", + }) + ) + ); + expect(wrapper.find(".aChild")).toHaveLength(1); + }); + + it("passes additionalClass to child div class", () => { + const additionalClass = "testAddon"; + const wrapper = shallow( + React.createElement(Modal, { + additionalClass, + handleClose: () => {}, + }) + ); + expect(wrapper.find(`.modal-wrapper .${additionalClass}`)).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..7150f4afe8 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js @@ -0,0 +1,212 @@ +/* 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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { mount } from "enzyme"; + +import Popover from "../Popover"; + +describe("Popover", () => { + const onMouseLeave = jest.fn(); + const onKeyDown = jest.fn(); + const editorRef = { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 100, + top: 250, + right: 0, + bottom: 0, + left: 20, + }; + }, + }; + + const targetRef = { + getBoundingClientRect() { + return { + x: 0, + y: 0, + width: 100, + height: 100, + top: 250, + right: 0, + bottom: 0, + left: 20, + }; + }, + }; + const targetPosition = { + x: 100, + y: 200, + width: 300, + height: 300, + top: 50, + right: 0, + bottom: 0, + left: 200, + }; + const popover = mount( + React.createElement( + Popover, + { + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editorRef, + targetPosition: targetPosition, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Poppy!") + ) + ); + + const tooltip = mount( + React.createElement( + Popover, + { + type: "tooltip", + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editorRef, + targetPosition: targetPosition, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Toolie!") + ) + ); + + beforeEach(() => { + onMouseLeave.mockClear(); + onKeyDown.mockClear(); + }); + + it("render", () => expect(popover).toMatchSnapshot()); + + it("render (tooltip)", () => expect(tooltip).toMatchSnapshot()); + + it("mount popover", () => { + const mountedPopover = mount( + React.createElement( + Popover, + { + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editorRef, + targetPosition: targetPosition, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Poppy!") + ) + ); + expect(mountedPopover).toMatchSnapshot(); + }); + + it("mount tooltip", () => { + const mountedTooltip = mount( + React.createElement( + Popover, + { + type: "tooltip", + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editorRef, + targetPosition: targetPosition, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Toolie!") + ) + ); + expect(mountedTooltip).toMatchSnapshot(); + }); + + it("tooltip normally displays above the target", () => { + const editor = { + getBoundingClientRect() { + return { + width: 500, + height: 500, + top: 0, + bottom: 500, + left: 0, + right: 500, + }; + }, + }; + const target = { + width: 30, + height: 10, + top: 100, + bottom: 110, + left: 20, + right: 50, + }; + + const mountedTooltip = mount( + React.createElement( + Popover, + { + type: "tooltip", + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editor, + targetPosition: target, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Toolie!") + ) + ); + + const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10); + expect(toolTipTop).toBeLessThanOrEqual(target.top); + }); + + it("tooltop won't display above the target when insufficient space", () => { + const editor = { + getBoundingClientRect() { + return { + width: 100, + height: 100, + top: 0, + bottom: 100, + left: 0, + right: 100, + }; + }, + }; + const target = { + width: 30, + height: 10, + top: 0, + bottom: 10, + left: 20, + right: 50, + }; + + const mountedTooltip = mount( + React.createElement( + Popover, + { + type: "tooltip", + onMouseLeave: onMouseLeave, + onKeyDown: onKeyDown, + editorRef: editor, + targetPosition: target, + mouseout: () => {}, + target: targetRef, + }, + React.createElement("h1", null, "Toolie!") + ) + ); + + 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..62f635acc1 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js @@ -0,0 +1,130 @@ +/* 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 . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; +import PreviewFunction from "../PreviewFunction"; + +function render(props) { + return shallow(React.createElement(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: "", + displayName: "chuck", + }; + 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..4cdc85fb23 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js @@ -0,0 +1,48 @@ +/* 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 . */ + +import React from "devtools/client/shared/vendor/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(React.createElement(ResultList, payload)); + wrapper.childAt(selectedIndex).simulate("click"); + expect(selectItem).toHaveBeenCalled(); + }); + + it("should render the component", () => { + const wrapper = shallow(React.createElement(ResultList, payload)); + expect(wrapper).toMatchSnapshot(); + }); + + it("selected index should have 'selected class'", () => { + const wrapper = shallow(React.createElement(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..c4c3990771 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import React from "devtools/client/shared/vendor/react"; +import { shallow } from "enzyme"; +import configureStore from "redux-mock-store"; + +import SearchInput from "../SearchInput"; + +describe("SearchInput", () => { + // !! wrapper is defined outside test scope + // so it will keep values between tests + const mockStore = configureStore([]); + const store = mockStore({ + ui: { mutableSearchOptions: { "foo-search": {} } }, + }); + const wrapper = shallow( + React.createElement(SearchInput, { + store: store, + query: "", + count: 5, + placeholder: "A placeholder", + summaryMsg: "So many results", + showErrorEmoji: false, + isLoading: false, + onChange: () => {}, + onKeyDown: () => {}, + searchKey: "foo-search", + showSearchModifiers: false, + showExcludePatterns: false, + showClose: true, + handleClose: jest.fn(), + setSearchOptions: jest.fn(), + }) + ).dive(); + + it("renders", () => expect(wrapper).toMatchSnapshot()); + + it("shows nav buttons", () => { + wrapper.setProps({ + handleNext: jest.fn(), + handlePrev: jest.fn(), + }); + expect(wrapper).toMatchSnapshot(); + }); + + it("shows svg error emoji", () => { + wrapper.setProps({ showErrorEmoji: true }); + expect(wrapper).toMatchSnapshot(); + }); + + it("shows svg magnifying glass", () => { + wrapper.setProps({ showErrorEmoji: false }); + expect(wrapper).toMatchSnapshot(); + }); + + describe("with optional onHistoryScroll", () => { + const searches = ["foo", "bar", "baz"]; + const createSearch = term => ({ + target: { value: term }, + key: "Enter", + }); + + const scrollUp = currentTerm => ({ + key: "ArrowUp", + target: { value: currentTerm }, + preventDefault: jest.fn(), + }); + const scrollDown = currentTerm => ({ + key: "ArrowDown", + target: { value: currentTerm }, + preventDefault: jest.fn(), + }); + + it("stores entered history in state", () => { + wrapper.setProps({ + onHistoryScroll: jest.fn(), + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + }); + + it("stores scroll history in state", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + expect(wrapper.state().history[1]).toEqual(searches[1]); + }); + + it("scrolls up stored history on arrow up", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[1])); + expect(wrapper.state().history[0]).toEqual(searches[0]); + expect(wrapper.state().history[1]).toEqual(searches[1]); + expect(onHistoryScroll).toHaveBeenCalledWith(searches[0]); + }); + + it("scrolls down stored history on arrow down", () => { + const onHistoryScroll = jest.fn(); + wrapper.setProps({ + onHistoryScroll, + onKeyDown: jest.fn(), + }); + wrapper.find("input").simulate("keyDown", createSearch(searches[0])); + wrapper.find("input").simulate("keyDown", createSearch(searches[1])); + wrapper.find("input").simulate("keyDown", createSearch(searches[2])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[2])); + wrapper.find("input").simulate("keyDown", scrollUp(searches[1])); + wrapper.find("input").simulate("keyDown", scrollDown(searches[0])); + expect(onHistoryScroll.mock.calls[2][0]).toBe(searches[1]); + }); + }); +}); diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap new file mode 100644 index 0000000000..abd8f10f51 --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Accordion basic render 1`] = ` +
+ + + +
+`; 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`] = ` + + 3 + +`; 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`] = ` +
+`; + +exports[`BracketArrow render up 1`] = ` +
+`; 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`] = ` +
+
+
+
+ +
+
+`; 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..c8534c4032 --- /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`] = ` +
+
+
+`; 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`] = ` + +
+ +
+ +
+ +
+
+ + + +
+

+ Poppy! +

+
+ } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="popover" + > + + + +
+
+

+ Poppy! +

+
+ +`; + +exports[`Popover mount tooltip 1`] = ` + +
+
+ +
+ + + +
+

+ Toolie! +

+
+ } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="tooltip" + > + + + + +
+

+ Toolie! +

+
+
+`; + +exports[`Popover render (tooltip) 1`] = ` + +
+
+ +
+ + + +
+

+ Toolie! +

+
+ } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="tooltip" + > + + + + +
+

+ Toolie! +

+
+ +`; + +exports[`Popover render 1`] = ` + +
+ +
+ +
+ +
+
+ + + +
+

+ Poppy! +

+
+ } + token={ + Object { + "getBoundingClientRect": [Function], + } + } + type="popover" + > + + + +
+
+

+ Poppy! +

+
+ +`; 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`] = ` + + + <anonymous> + + + ( + + + ) + + +`; 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`] = ` +
    +
  • +
    + title +
    +
    + subtitle +
    +
  • +
  • +
    + title 1 +
    +
    + subtitle 1 +
    +
  • +
+`; diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap new file mode 100644 index 0000000000..c56a13dc3b --- /dev/null +++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchInput renders 1`] = ` +
+
+ + +
+ So many results +
+
+ + +
+
+
+`; + +exports[`SearchInput shows nav buttons 1`] = ` +
+
+ + +
+ So many results +
+
+ + +
+
+ + +
+
+
+`; + +exports[`SearchInput shows svg error emoji 1`] = ` +
+
+ + +
+ So many results +
+
+ + +
+
+ + +
+
+
+`; + +exports[`SearchInput shows svg magnifying glass 1`] = ` +
+
+ + +
+ So many results +
+
+ + +
+
+ + +
+
+
+`; -- cgit v1.2.3