Bug 1419081 - Open the sidebar when doing ctrl-click on an ObjectInspector. r=nchevobbe draft
authorMike Park <mikeparkms@gmail.com>
Thu, 30 Nov 2017 13:36:39 -0500
changeset 706994 3654fc7ff894e214c81075c363d9524dcfcc7f4d
parent 705715 86e4d9d10c3ae9374cab516af38a9d63b0270f40
child 742826 a605bd4302b42b6cde68e79f79280cd8545ae6d3
push id91988
push userbmo:mpark@mozilla.com
push dateMon, 04 Dec 2017 16:56:32 +0000
reviewersnchevobbe
bugs1419081
milestone59.0a1
Bug 1419081 - Open the sidebar when doing ctrl-click on an ObjectInspector. r=nchevobbe MozReview-Commit-ID: BRLQnvVdcr7
devtools/client/themes/webconsole.css
devtools/client/webconsole/new-console-output/actions/ui.js
devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
devtools/client/webconsole/new-console-output/components/FilterBar.js
devtools/client/webconsole/new-console-output/components/GripMessageBody.js
devtools/client/webconsole/new-console-output/components/MessageContainer.js
devtools/client/webconsole/new-console-output/components/SideBar.js
devtools/client/webconsole/new-console-output/components/message-types/ConsoleApiCall.js
devtools/client/webconsole/new-console-output/components/message-types/EvaluationResult.js
devtools/client/webconsole/new-console-output/constants.js
devtools/client/webconsole/new-console-output/reducers/ui.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_close_sidebar.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_sidebar.js
devtools/client/webconsole/new-console-output/test/store/ui.test.js
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -1203,27 +1203,29 @@ body #output-container {
   /* Set to prevent the sidebar from extending past the right edge of the page */
   width: unset;
 }
 
 .sidebar-wrapper {
   display: grid;
   grid-template-rows: auto 1fr;
   width: 100%;
+  overflow: hidden;
 }
 
 .webconsole-sidebar-toolbar {
   grid-row: 1 / 2;
   min-height: var(--primary-toolbar-height);
   display: flex;
   justify-content: end;
 }
 
 .sidebar-contents {
   grid-row: 2 / 3;
+  overflow: scroll;
 }
 
 .webconsole-sidebar-toolbar .sidebar-close-button {
   padding: 4px 0;
   margin: 0;
   margin-inline-end: -3px;
 }
 
--- a/devtools/client/webconsole/new-console-output/actions/ui.js
+++ b/devtools/client/webconsole/new-console-output/actions/ui.js
@@ -10,17 +10,18 @@ const { getAllUi } = require("devtools/c
 const Services = require("Services");
 
 const {
   FILTER_BAR_TOGGLE,
   INITIALIZE,
   PERSIST_TOGGLE,
   PREFS,
   SELECT_NETWORK_MESSAGE_TAB,
-  SIDEBAR_TOGGLE,
+  SIDEBAR_CLOSE,
+  SHOW_OBJECT_IN_SIDEBAR,
   TIMESTAMPS_TOGGLE,
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 function filterBarToggle(show) {
   return (dispatch, getState) => {
     dispatch({
       type: FILTER_BAR_TOGGLE,
     });
@@ -54,22 +55,30 @@ function selectNetworkMessageTab(id) {
 }
 
 function initialize() {
   return {
     type: INITIALIZE
   };
 }
 
-function sidebarToggle(show) {
+function sidebarClose(show) {
   return {
-    type: SIDEBAR_TOGGLE,
+    type: SIDEBAR_CLOSE,
+  };
+}
+
+function showObjectInSidebar(grip) {
+  return {
+    type: SHOW_OBJECT_IN_SIDEBAR,
+    grip
   };
 }
 
 module.exports = {
   filterBarToggle,
   initialize,
   persistToggle,
   selectNetworkMessageTab,
-  sidebarToggle,
+  sidebarClose,
+  showObjectInSidebar,
   timestampsToggle,
 };
--- a/devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
+++ b/devtools/client/webconsole/new-console-output/components/ConsoleOutput.js
@@ -40,16 +40,17 @@ class ConsoleOutput extends Component {
       dispatch: PropTypes.func.isRequired,
       timestampsVisible: PropTypes.bool,
       messagesTableData: PropTypes.object.isRequired,
       messagesRepeat: PropTypes.object.isRequired,
       networkMessagesUpdate: PropTypes.object.isRequired,
       visibleMessages: PropTypes.array.isRequired,
       networkMessageActiveTabId: PropTypes.string.isRequired,
       onFirstMeaningfulPaint: PropTypes.func.isRequired,
+      sidebarToggle: PropTypes.bool,
     };
   }
 
   constructor(props) {
     super(props);
     this.onContextMenu = this.onContextMenu.bind(this);
   }
 
@@ -124,16 +125,17 @@ class ConsoleOutput extends Component {
       messagesUi,
       messagesTableData,
       messagesRepeat,
       networkMessagesUpdate,
       networkMessageActiveTabId,
       serviceContainer,
       timestampsVisible,
       initialized,
+      sidebarToggle
     } = this.props;
 
     if (!initialized) {
       const numberMessagesFitViewport = getInitialMessageCountForViewport(window);
       if (numberMessagesFitViewport < visibleMessages.length) {
         visibleMessages = visibleMessages.slice(
           visibleMessages.length - numberMessagesFitViewport);
       }
@@ -146,16 +148,17 @@ class ConsoleOutput extends Component {
       serviceContainer,
       open: messagesUi.includes(messageId),
       tableData: messagesTableData.get(messageId),
       timestampsVisible,
       repeat: messagesRepeat[messageId],
       networkMessageUpdate: networkMessagesUpdate[messageId],
       networkMessageActiveTabId,
       getMessage: () => messages.get(messageId),
+      sidebarToggle,
     }));
 
     return (
       dom.div({
         className: "webconsole-output",
         onContextMenu: this.onContextMenu,
         ref: node => {
           this.outputNode = node;
@@ -183,16 +186,17 @@ function mapStateToProps(state, props) {
     messages: getAllMessagesById(state),
     visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     messagesRepeat: getAllRepeatById(state),
     networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
     timestampsVisible: state.ui.timestampsVisible,
     networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
+    sidebarToggle: state.prefs.sidebarToggle,
   };
 }
 
 module.exports = connect(mapStateToProps)(props =>
   VisibilityHandler(
     null,
     createElement(ConsoleOutput, props)
   )
--- a/devtools/client/webconsole/new-console-output/components/FilterBar.js
+++ b/devtools/client/webconsole/new-console-output/components/FilterBar.js
@@ -27,25 +27,23 @@ class FilterBar extends Component {
       dispatch: PropTypes.func.isRequired,
       filter: PropTypes.object.isRequired,
       serviceContainer: PropTypes.shape({
         attachRefToHud: PropTypes.func.isRequired,
       }).isRequired,
       filterBarVisible: PropTypes.bool.isRequired,
       persistLogs: PropTypes.bool.isRequired,
       filteredMessagesCount: PropTypes.object.isRequired,
-      sidebarToggle: PropTypes.bool,
     };
   }
 
   constructor(props) {
     super(props);
     this.onClickMessagesClear = this.onClickMessagesClear.bind(this);
     this.onClickFilterBarToggle = this.onClickFilterBarToggle.bind(this);
-    this.onClickSidebarToggle = this.onClickSidebarToggle.bind(this);
     this.onClickRemoveAllFilters = this.onClickRemoveAllFilters.bind(this);
     this.onClickRemoveTextFilter = this.onClickRemoveTextFilter.bind(this);
     this.onSearchInput = this.onSearchInput.bind(this);
     this.onChangePersistToggle = this.onChangePersistToggle.bind(this);
     this.renderFiltersConfigBar = this.renderFiltersConfigBar.bind(this);
     this.renderFilteredMessagesBar = this.renderFilteredMessagesBar.bind(this);
   }
 
@@ -82,20 +80,16 @@ class FilterBar extends Component {
   onClickMessagesClear() {
     this.props.dispatch(actions.messagesClear());
   }
 
   onClickFilterBarToggle() {
     this.props.dispatch(actions.filterBarToggle());
   }
 
-  onClickSidebarToggle() {
-    this.props.dispatch(actions.sidebarToggle());
-  }
-
   onClickRemoveAllFilters() {
     this.props.dispatch(actions.defaultFiltersReset());
   }
 
   onClickRemoveTextFilter() {
     this.props.dispatch(actions.filterTextSet(""));
   }
 
@@ -221,17 +215,16 @@ class FilterBar extends Component {
   }
 
   render() {
     const {
       filter,
       filterBarVisible,
       persistLogs,
       filteredMessagesCount,
-      sidebarToggle,
     } = this.props;
 
     let children = [
       dom.div({
         className: "devtools-toolbar webconsole-filterbar-primary",
         key: "primary-bar",
       },
         dom.button({
@@ -256,23 +249,16 @@ class FilterBar extends Component {
           onInput: this.onSearchInput
         }),
         FilterCheckbox({
           label: l10n.getStr("webconsole.enablePersistentLogs.label"),
           title: l10n.getStr("webconsole.enablePersistentLogs.tooltip"),
           onChange: this.onChangePersistToggle,
           checked: persistLogs,
         }),
-        sidebarToggle ?
-          dom.button({
-            className: "devtools-button webconsole-sidebar-button",
-            title: l10n.getStr("webconsole.toggleFilterButton.tooltip"),
-            onClick: this.onClickSidebarToggle
-          }, "Toggle Sidebar")
-          : null,
       )
     ];
 
     if (filteredMessagesCount.global > 0) {
       children.push(this.renderFilteredMessagesBar());
     }
 
     if (filterBarVisible) {
@@ -293,13 +279,12 @@ class FilterBar extends Component {
 
 function mapStateToProps(state) {
   let uiState = getAllUi(state);
   return {
     filter: getAllFilters(state),
     filterBarVisible: uiState.filterBarVisible,
     persistLogs: uiState.persistLogs,
     filteredMessagesCount: getFilteredMessagesCount(state),
-    sidebarToggle: state.prefs.sidebarToggle,
   };
 }
 
 module.exports = connect(mapStateToProps)(FilterBar);
--- a/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
+++ b/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
@@ -9,60 +9,66 @@
 // If this is being run from Mocha, then the browser loader hasn't set up
 // define. We need to do that before loading Rep.
 if (typeof define === "undefined") {
   require("amd-loader");
 }
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const ObjectClient = require("devtools/shared/client/object-client");
 const {
   MESSAGE_TYPE,
   JSTERM_COMMANDS,
 } = require("../constants");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 const reps = require("devtools/client/shared/components/reps/reps");
 const { REPS, MODE } = reps;
 const ObjectInspector = createFactory(reps.ObjectInspector);
 const { Grip } = REPS;
 
 GripMessageBody.displayName = "GripMessageBody";
 
 GripMessageBody.propTypes = {
+  dispatch: PropTypes.func.isRequired,
   grip: PropTypes.oneOfType([
     PropTypes.string,
     PropTypes.number,
     PropTypes.object,
   ]).isRequired,
   serviceContainer: PropTypes.shape({
     createElement: PropTypes.func.isRequired,
     hudProxy: PropTypes.object.isRequired,
     onViewSourceInDebugger: PropTypes.func.isRequired,
   }),
   userProvidedStyle: PropTypes.string,
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   type: PropTypes.string,
   helperType: PropTypes.string,
+  sidebarToggle: PropTypes.bool,
 };
 
 GripMessageBody.defaultProps = {
   mode: MODE.LONG,
 };
 
 function GripMessageBody(props) {
   const {
     grip,
     userProvidedStyle,
     serviceContainer,
     useQuotes,
     escapeWhitespace,
     mode = MODE.LONG,
+    dispatch,
+    sidebarToggle,
   } = props;
 
   let styleObject;
   if (userProvidedStyle && userProvidedStyle !== "") {
     styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
   }
 
   let onDOMNodeMouseOver;
@@ -77,16 +83,23 @@ function GripMessageBody(props) {
       ? (object, e) => {
         // Stop the event propagation so we don't trigger ObjectInspector expand/collapse.
         e.stopPropagation();
         serviceContainer.openNodeInInspector(object);
       }
       : null;
   }
 
+  function onClickCapture(e) {
+    if (grip.actor && (e.ctrlKey || e.metaKey)) {
+      dispatch(actions.showObjectInSidebar(grip));
+      e.stopPropagation();
+    }
+  }
+
   const objectInspectorProps = {
     // Auto-expand the ObjectInspector when the message is a console.dir one.
     autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
     mode,
     // TODO: we disable focus since it's not currently working well in ObjectInspector.
     // Let's remove the property below when problem are fixed in OI.
     disabledFocus: true,
     roots: [{
@@ -117,17 +130,19 @@ function GripMessageBody(props) {
     Object.assign(objectInspectorProps, {
       onDOMNodeMouseOver,
       onDOMNodeMouseOut,
       onInspectIconClick,
       defaultRep: Grip,
     });
   }
 
-  return ObjectInspector(objectInspectorProps);
+  return sidebarToggle
+         ? dom.span({onClickCapture}, ObjectInspector(objectInspectorProps))
+         : ObjectInspector(objectInspectorProps);
 }
 
 // Regular expression that matches the allowed CSS property names.
 const allowedStylesRegex = new RegExp(
   "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
   "margin|padding|text|transition|outline|white-space|word|writing|" +
   "(?:min-|max-)?width|(?:min-|max-)?height)"
 );
--- a/devtools/client/webconsole/new-console-output/components/MessageContainer.js
+++ b/devtools/client/webconsole/new-console-output/components/MessageContainer.js
@@ -30,16 +30,17 @@ class MessageContainer extends Component
       messageId: PropTypes.string.isRequired,
       open: PropTypes.bool.isRequired,
       serviceContainer: PropTypes.object.isRequired,
       tableData: PropTypes.object,
       timestampsVisible: PropTypes.bool.isRequired,
       repeat: PropTypes.number,
       networkMessageUpdate: PropTypes.object,
       getMessage: PropTypes.func.isRequired,
+      sidebarToggle: PropTypes.bool,
     };
   }
 
   static get defaultProps() {
     return {
       open: false,
     };
   }
--- a/devtools/client/webconsole/new-console-output/components/SideBar.js
+++ b/devtools/client/webconsole/new-console-output/components/SideBar.js
@@ -9,48 +9,50 @@ const PropTypes = require("devtools/clie
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 
 class SideBar extends Component {
   static get propTypes() {
     return {
       dispatch: PropTypes.func.isRequired,
-      sidebarVisible: PropTypes.bool
+      sidebarVisible: PropTypes.bool,
+      grip: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
-    this.onClickSidebarToggle = this.onClickSidebarToggle.bind(this);
+    this.onClickSidebarClose = this.onClickSidebarClose.bind(this);
   }
 
-  onClickSidebarToggle() {
-    this.props.dispatch(actions.sidebarToggle());
+  onClickSidebarClose() {
+    this.props.dispatch(actions.sidebarClose());
   }
 
   render() {
     let {
       sidebarVisible,
+      grip,
     } = this.props;
 
     let endPanel = dom.aside({
       className: "sidebar-wrapper"
     },
       dom.header({
         className: "devtools-toolbar webconsole-sidebar-toolbar"
       },
         dom.button({
           className: "devtools-button sidebar-close-button",
-          onClick: this.onClickSidebarToggle
+          onClick: this.onClickSidebarClose
         })
       ),
       dom.aside({
         className: "sidebar-contents"
-      }, "Sidebar WIP")
+      }, JSON.stringify(grip, null, 2))
     );
 
     return (
       sidebarVisible ?
         SplitBox({
           className: "sidebar",
           endPanel,
           endPanelControl: true,
@@ -61,12 +63,13 @@ class SideBar extends Component {
         : null
     );
   }
 }
 
 function mapStateToProps(state, props) {
   return {
     sidebarVisible: state.ui.sidebarVisible,
+    grip: state.ui.gripInSidebar,
   };
 }
 
 module.exports = connect(mapStateToProps)(SideBar);
--- a/devtools/client/webconsole/new-console-output/components/message-types/ConsoleApiCall.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/ConsoleApiCall.js
@@ -19,31 +19,33 @@ const Message = createFactory(require("d
 ConsoleApiCall.displayName = "ConsoleApiCall";
 
 ConsoleApiCall.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   serviceContainer: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
+  sidebarToggle: PropTypes.bool,
 };
 
 ConsoleApiCall.defaultProps = {
   open: false,
 };
 
 function ConsoleApiCall(props) {
   const {
     dispatch,
     message,
     open,
     tableData,
     serviceContainer,
     timestampsVisible,
     repeat,
+    sidebarToggle,
   } = props;
   const {
     id: messageId,
     indent,
     source,
     type,
     level,
     stacktrace,
@@ -57,16 +59,17 @@ function ConsoleApiCall(props) {
   let messageBody;
   const messageBodyConfig = {
     dispatch,
     messageId,
     parameters,
     userProvidedStyles,
     serviceContainer,
     type,
+    sidebarToggle,
   };
 
   if (type === "trace") {
     messageBody = dom.span({className: "cm-variable"}, "console.trace()");
   } else if (type === "assert") {
     let reps = formatReps(messageBodyConfig);
     messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps);
   } else if (type === "table") {
@@ -125,32 +128,34 @@ function formatReps(options = {}) {
     dispatch,
     loadedObjectProperties,
     loadedObjectEntries,
     messageId,
     parameters,
     serviceContainer,
     userProvidedStyles,
     type,
+    sidebarToggle,
   } = options;
 
   return (
     parameters
       // Get all the grips.
       .map((grip, key) => GripMessageBody({
         dispatch,
         messageId,
         grip,
         key,
         userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null,
         serviceContainer,
         useQuotes: false,
         loadedObjectProperties,
         loadedObjectEntries,
         type,
+        sidebarToggle,
       }))
       // Interleave spaces.
       .reduce((arr, v, i) => {
         // We need to interleave a space if we are not on the last element AND
         // if we are not between 2 messages with user provided style.
         const needSpace = i + 1 < parameters.length &&
           (!userProvidedStyles || !userProvidedStyles[i] || !userProvidedStyles[i + 1]);
 
--- a/devtools/client/webconsole/new-console-output/components/message-types/EvaluationResult.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/EvaluationResult.js
@@ -14,24 +14,26 @@ const GripMessageBody = require("devtool
 
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
+  sidebarToggle: PropTypes.bool,
 };
 
 function EvaluationResult(props) {
   const {
     dispatch,
     message,
     serviceContainer,
     timestampsVisible,
+    sidebarToggle,
   } = props;
 
   const {
     source,
     type,
     helperType,
     level,
     id: messageId,
@@ -58,16 +60,17 @@ function EvaluationResult(props) {
       dispatch,
       messageId,
       grip: parameters[0],
       serviceContainer,
       useQuotes: true,
       escapeWhitespace: false,
       type,
       helperType,
+      sidebarToggle,
     });
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
   return Message({
     source,
     type,
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -19,17 +19,18 @@ const actionTypes = {
   MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
   MESSAGES_ADD: "MESSAGES_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
   NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
   NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
   PERSIST_TOGGLE: "PERSIST_TOGGLE",
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
   SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
-  SIDEBAR_TOGGLE: "SIDEBAR_TOGGLE",
+  SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
+  SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
 };
 
 const prefs = {
   PREFS: {
     FILTER: {
       ERROR: "devtools.webconsole.filter.error",
       WARN: "devtools.webconsole.filter.warn",
--- a/devtools/client/webconsole/new-console-output/reducers/ui.js
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -5,50 +5,60 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {
   FILTER_BAR_TOGGLE,
   INITIALIZE,
   PERSIST_TOGGLE,
   SELECT_NETWORK_MESSAGE_TAB,
-  SIDEBAR_TOGGLE,
+  SIDEBAR_CLOSE,
+  SHOW_OBJECT_IN_SIDEBAR,
   TIMESTAMPS_TOGGLE,
   MESSAGES_CLEAR,
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 const {
   PANELS,
 } = require("devtools/client/netmonitor/src/constants");
 
 const UiState = (overrides) => Object.freeze(Object.assign({
   filterBarVisible: false,
   initialized: false,
   networkMessageActiveTabId: PANELS.HEADERS,
   persistLogs: false,
   sidebarVisible: false,
   timestampsVisible: true,
+  gripInSidebar: null
 }, overrides));
 
 function ui(state = UiState(), action) {
   switch (action.type) {
     case FILTER_BAR_TOGGLE:
       return Object.assign({}, state, {filterBarVisible: !state.filterBarVisible});
     case PERSIST_TOGGLE:
       return Object.assign({}, state, {persistLogs: !state.persistLogs});
     case TIMESTAMPS_TOGGLE:
       return Object.assign({}, state, {timestampsVisible: action.visible});
     case SELECT_NETWORK_MESSAGE_TAB:
       return Object.assign({}, state, {networkMessageActiveTabId: action.id});
-    case SIDEBAR_TOGGLE:
-      return Object.assign({}, state, {sidebarVisible: !state.sidebarVisible});
+    case SIDEBAR_CLOSE:
+      return Object.assign({}, state, {
+        sidebarVisible: !state.sidebarVisible,
+        gripInSidebar: null
+      });
     case INITIALIZE:
       return Object.assign({}, state, {initialized: true});
     case MESSAGES_CLEAR:
-      return Object.assign({}, state, {sidebarVisible: false});
+      return Object.assign({}, state, {sidebarVisible: false, gripInSidebar: null});
+    case SHOW_OBJECT_IN_SIDEBAR:
+      if (action.grip === state.gripInSidebar) {
+        return state;
+      }
+      return Object.assign({}, state, {sidebarVisible: true, gripInSidebar: action.grip});
   }
 
   return state;
 }
 
 module.exports = {
   UiState,
   ui,
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -397,16 +397,17 @@ skip-if = true #	Bug 1403454
 # old console skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_scroll.js]
 [browser_webconsole_select_all.js]
 skip-if = true #	Bug 1404359
 [browser_webconsole_show_subresource_security_errors.js]
 skip-if = true # Bug 1408948
 # old console skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243987
 [browser_webconsole_shows_reqs_in_netmonitor.js]
+[browser_webconsole_sidebar.js]
 [browser_webconsole_sourcemap_css.js]
 [browser_webconsole_sourcemap_error.js]
 [browser_webconsole_sourcemap_invalid.js]
 [browser_webconsole_sourcemap_nosource.js]
 [browser_webconsole_split.js]
 skip-if = true # Bug 1408949
 [browser_webconsole_split_escape_key.js]
 skip-if = true #	Bug 1405647
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_close_sidebar.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_close_sidebar.js
@@ -47,14 +47,22 @@ add_task(async function () {
   }
   synthesizeKeyShortcut(clearShortcut);
   await waitFor(() => findMessages(hud, "").length == 0);
   sidebar = hud.ui.document.querySelector(".sidebar");
   ok(!sidebar, "Sidebar hidden after console.clear()");
 });
 
 async function showSidebar(hud) {
-  let toggleButton = hud.ui.document.querySelector(".webconsole-sidebar-button");
+  let onMessage = waitForMessage(hud, "Object");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+    content.wrappedJSObject.console.log({a: 1});
+  });
+  await onMessage;
+
+  let objectNode = hud.ui.outputNode.querySelector(".object-inspector");
   let wrapper = hud.ui.document.querySelector(".webconsole-output-wrapper");
   let onSidebarShown = waitForNodeMutation(wrapper, { childList: true });
-  toggleButton.click();
+
+  EventUtils.synthesizeMouseAtCenter(objectNode,
+    {[Services.appinfo.OS == "Darwin" ? "metaKey" : "ctrlKey"]: true}, hud.ui.window);
   await onSidebarShown;
 }
copy from devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_close_sidebar.js
copy to devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_sidebar.js
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_close_sidebar.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_sidebar.js
@@ -2,59 +2,94 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that the sidebar is hidden when the console is cleared.
 
 "use strict";
 
-const TEST_URI = "data:text/html;charset=utf8,";
+const TEST_URI =
+  "data:text/html;charset=utf8," +
+  "<script>console.log({a:1},100,{b:1},'foo',false,null,undefined);</script>";
 
 add_task(async function () {
   // Should be removed when sidebar work is complete
   await SpecialPowers.pushPrefEnv({"set": [
     ["devtools.webconsole.sidebarToggle", true]
   ]});
 
   let hud = await openNewTabAndConsole(TEST_URI);
-  await showSidebar(hud);
+
+  let message = findMessage(hud, "foo");
+  let [objectA, objectB] = message.querySelectorAll(".object-inspector");
+  let number = findMessage(hud, "100", ".objectBox");
+  let string = findMessage(hud, "foo", ".objectBox");
+  let bool = findMessage(hud, "false", ".objectBox");
+  let nullMessage = findMessage(hud, "null", ".objectBox");
+  let undefinedMsg = findMessage(hud, "undefined", ".objectBox");
+
+  info("Showing sidebar for {a:1}");
+  await showSidebar(hud, objectA, true);
 
-  info("Click the clear console button");
-  let clearButton = hud.ui.document.querySelector(".devtools-button");
-  clearButton.click();
-  await waitFor(() => findMessages(hud, "").length == 0);
-  let sidebar = hud.ui.document.querySelector(".sidebar");
-  ok(!sidebar, "Sidebar hidden after clear console button clicked");
+  let sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
+  ok(sidebarText.includes('"a":'), "Sidebar is shown for {a:1}");
 
-  await showSidebar(hud);
+  info("Showing sidebar for {a:1} again");
+  await showSidebar(hud, objectA, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on same object");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on same object");
+
+  info("Showing sidebar for {b:1}");
+  await showSidebar(hud, objectB, false);
+  isnot(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+        "Sidebar is updated for {b:1}");
+  sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
+  ok(sidebarText.includes('"b":'), "Sidebar contents shown for {b:1}");
 
-  info("Send a console.clear()");
-  let onMessagesCleared = waitForMessage(hud, "Console was cleared");
-  ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
-    content.wrappedJSObject.console.clear();
-  });
-  await onMessagesCleared;
-  sidebar = hud.ui.document.querySelector(".sidebar");
-  ok(!sidebar, "Sidebar hidden after console.clear()");
+  info("Ctrl-clicking on number");
+  await showSidebar(hud, number, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on number");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on number");
 
-  await showSidebar(hud);
+  info("Ctrl-clicking on string");
+  await showSidebar(hud, string, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on string");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on string");
 
-  info("Send ctrl-l to clear console");
-  let clearShortcut;
-  if (Services.appinfo.OS === "Darwin") {
-    clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX");
-  } else {
-    clearShortcut = WCUL10n.getStr("webconsole.clear.key");
-  }
-  synthesizeKeyShortcut(clearShortcut);
-  await waitFor(() => findMessages(hud, "").length == 0);
-  sidebar = hud.ui.document.querySelector(".sidebar");
-  ok(!sidebar, "Sidebar hidden after console.clear()");
+  info("Ctrl-clicking on bool");
+  await showSidebar(hud, bool, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on bool");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on bool");
+
+  info("Ctrl-clicking on null message");
+  await showSidebar(hud, nullMessage, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on null message");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on null message");
+
+  info("Ctrl-clicking on undefined message");
+  await showSidebar(hud, undefinedMsg, false);
+  ok(hud.ui.document.querySelector(".sidebar"),
+     "Sidebar is still shown after clicking on undefined message");
+  is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
+     "Sidebar is not updated after clicking on undefined message");
 });
 
-async function showSidebar(hud) {
-  let toggleButton = hud.ui.document.querySelector(".webconsole-sidebar-button");
+async function showSidebar(hud, node, expectMutation) {
   let wrapper = hud.ui.document.querySelector(".webconsole-output-wrapper");
   let onSidebarShown = waitForNodeMutation(wrapper, { childList: true });
-  toggleButton.click();
-  await onSidebarShown;
+
+  EventUtils.synthesizeMouseAtCenter(node,
+    {[Services.appinfo.OS == "Darwin" ? "metaKey" : "ctrlKey"]: true}, hud.ui.window);
+  if (expectMutation) {
+    await onSidebarShown;
+  }
 }
--- a/devtools/client/webconsole/new-console-output/test/store/ui.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/ui.test.js
@@ -2,36 +2,73 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const expect = require("expect");
 
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
 describe("Testing UI", () => {
   let store;
 
   beforeEach(() => {
     store = setupStore();
   });
 
   describe("Toggle sidebar", () => {
     it("sidebar is toggled on and off", () => {
-      store.dispatch(actions.sidebarToggle());
+      store.dispatch(actions.sidebarClose());
       expect(store.getState().ui.sidebarVisible).toEqual(true);
-      store.dispatch(actions.sidebarToggle());
+      store.dispatch(actions.sidebarClose());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
     });
   });
 
   describe("Hide sidebar on clear", () => {
     it("sidebar is hidden on clear", () => {
-      store.dispatch(actions.sidebarToggle());
+      store.dispatch(actions.sidebarClose());
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       store.dispatch(actions.messagesClear());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
       store.dispatch(actions.messagesClear());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
     });
   });
+
+  describe("Show object in sidebar", () => {
+    it("sidebar is shown with correct object", () => {
+      const message = stubPreparedMessages.get("inspect({a: 1})");
+      store.dispatch(actions.showObjectInSidebar(message.parameters[0]));
+
+      expect(store.getState().ui.sidebarVisible).toEqual(true);
+      expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
+    });
+
+    it("sidebar is not updated for the same object", () => {
+      const message = stubPreparedMessages.get("inspect({a: 1})");
+      store.dispatch(actions.showObjectInSidebar(message.parameters[0]));
+
+      expect(store.getState().ui.sidebarVisible).toEqual(true);
+      expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
+      let state = store.getState().ui;
+
+      store.dispatch(actions.showObjectInSidebar(message.parameters[0]));
+      expect(store.getState().ui).toEqual(state);
+    });
+
+    it("sidebar shown and updated for new object", () => {
+      const message = stubPreparedMessages.get("inspect({a: 1})");
+      store.dispatch(actions.showObjectInSidebar(message.parameters[0]));
+
+      expect(store.getState().ui.sidebarVisible).toEqual(true);
+      expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
+
+      const newMessage = stubPreparedMessages.get("new Date(0)");
+      store.dispatch(actions.showObjectInSidebar(newMessage.parameters[0]));
+
+      expect(store.getState().ui.sidebarVisible).toEqual(true);
+      expect(store.getState().ui.gripInSidebar).toEqual(newMessage.parameters[0]);
+    });
+  });
 });