From 6e17955ce2e1f6d2c470202832576763de8f1375 Mon Sep 17 00:00:00 2001
From: Alexander Lesnenko <alxnddr@users.noreply.github.com>
Date: Tue, 15 Feb 2022 17:23:46 +0000
Subject: [PATCH] fix data picker another double scroll and add a visual test
 (#20524)

* fix data picker another double scroll and add a visual test

* review
---
 .../AccordionList/AccordionList.jsx           | 133 +++++++++---------
 .../notebook/notebook.cy.spec.js              |  25 +++-
 2 files changed, 90 insertions(+), 68 deletions(-)

diff --git a/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx b/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
index 0b7c8485517..84af8fece37 100644
--- a/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
+++ b/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
@@ -1,4 +1,5 @@
 import React, { Component } from "react";
+import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
 import { List, CellMeasurer, CellMeasurerCache } from "react-virtualized";
 
@@ -45,8 +46,6 @@ export default class AccordionList extends Component {
       fixedWidth: true,
       minHeight: 10,
     });
-
-    this.containerRef = React.createRef();
   }
 
   static propTypes = {
@@ -127,6 +126,8 @@ export default class AccordionList extends Component {
   };
 
   componentDidMount() {
+    this.container = ReactDOM.findDOMNode(this);
+
     // NOTE: for some reason the row heights aren't computed correctly when
     // first rendering, so force the list to update
     this._forceUpdateList();
@@ -134,11 +135,11 @@ export default class AccordionList extends Component {
     // Use list.scrollToRow instead of the scrollToIndex prop since the
     // causes the list's scrolling to be pinned to the selected row
     setTimeout(() => {
-      const hasFocusedChildren = this.containerRef?.current?.contains(
+      const hasFocusedChildren = this.container.contains(
         document.activeElement,
       );
       if (!hasFocusedChildren && this.props.hasInitialFocus) {
-        this.containerRef?.current?.focus();
+        this.container.focus();
       }
 
       const index = this._initialSelectedRowIndex;
@@ -272,7 +273,7 @@ export default class AccordionList extends Component {
 
     for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
       const section = sections[sectionIndex];
-      for (let itemIndex = 0; itemIndex < section.items.length; itemIndex++) {
+      for (let itemIndex = 0; itemIndex < section.items?.length; itemIndex++) {
         const item = section.items[itemIndex];
         if (itemIsSelected(item)) {
           return {
@@ -512,6 +513,8 @@ export default class AccordionList extends Component {
     );
   }
 
+  isVirtualized = () => this.props.maxHeight !== Infinity;
+
   canToggleSections = () => {
     const { alwaysTogglable, sections } = this.props;
     return alwaysTogglable || sections.length > 1;
@@ -539,7 +542,7 @@ export default class AccordionList extends Component {
   // Because of virtualization, focused search input can be removed which does not trigger blur event.
   // We need to restore focus on the component root container to make keyboard navigation working
   handleSearchRemoval = () => {
-    this.containerRef?.current?.focus();
+    this.container?.focus();
   };
 
   render() {
@@ -553,11 +556,10 @@ export default class AccordionList extends Component {
 
     const searchRowIndex = rows.findIndex(row => row.type === "search");
 
-    if (this.props.maxHeight === Infinity) {
+    if (!this.isVirtualized()) {
       return (
         <AccordionListRoot
           role="tree"
-          ref={this.containerRef}
           onKeyDown={this.handleKeyDown}
           tabIndex={-1}
           className={className}
@@ -603,68 +605,65 @@ export default class AccordionList extends Component {
       // HACK - Ensure the component can scroll
       // This is a temporary fix to handle cases where the parent component doesn’t pass in the correct `maxHeight`
       overflowY: "auto",
+      outline: "none",
     };
 
     return (
-      <AccordionListRoot
-        role="tree"
-        ref={this.containerRef}
-        onKeyDown={this.handleKeyDown}
-        tabIndex={-1}
-      >
-        <List
-          id={id}
-          ref={list => (this._list = list)}
-          className={className}
-          style={{ ...defaultListStyle, ...style }}
-          containerStyle={{ pointerEvents: "auto" }}
-          width={width}
-          height={height}
-          rowCount={rows.length}
-          deferredMeasurementCache={this._cache}
-          rowHeight={this._cache.rowHeight}
-          // HACK: needs to be large enough to render enough rows to fill the screen since we used
-          // the CellMeasurerCache to calculate the height
-          overscanRowCount={100}
-          scrollToIndex={scrollToIndex}
-          scrollToAlignment={scrollToAlignment}
-          rowRenderer={({ key, index, parent, style }) => {
-            return (
-              <CellMeasurer
-                cache={this._cache}
-                columnIndex={0}
-                key={key}
-                rowIndex={index}
-                parent={parent}
-              >
-                {({ measure }) => (
-                  <AccordionListCell
-                    hasCursor={this.isRowSelected(rows[index])}
-                    {...this.props}
-                    style={style}
-                    row={rows[index]}
-                    sections={sections}
-                    onChange={this.handleChange}
-                    searchText={this.state.searchText}
-                    onChangeSearchText={this.handleChangeSearchText}
-                    sectionIsExpanded={this.isSectionExpanded}
-                    canToggleSections={this.canToggleSections()}
-                    toggleSection={this.toggleSection}
-                  />
-                )}
-              </CellMeasurer>
-            );
-          }}
-          onRowsRendered={({ startIndex, stopIndex }) => {
-            this._startIndex = startIndex;
-            this._stopIndex = stopIndex;
-
-            if (searchRowIndex < startIndex || searchRowIndex > stopIndex) {
-              this.handleSearchRemoval();
-            }
-          }}
-        />
-      </AccordionListRoot>
+      <List
+        id={id}
+        ref={list => (this._list = list)}
+        className={className}
+        style={{ ...defaultListStyle, ...style }}
+        containerStyle={{ pointerEvents: "auto" }}
+        width={width}
+        height={height}
+        rowCount={rows.length}
+        deferredMeasurementCache={this._cache}
+        rowHeight={this._cache.rowHeight}
+        // HACK: needs to be large enough to render enough rows to fill the screen since we used
+        // the CellMeasurerCache to calculate the height
+        overscanRowCount={100}
+        scrollToIndex={scrollToIndex}
+        scrollToAlignment={scrollToAlignment}
+        containerProps={{
+          onKeyDown: this.handleKeyDown,
+        }}
+        rowRenderer={({ key, index, parent, style }) => {
+          return (
+            <CellMeasurer
+              cache={this._cache}
+              columnIndex={0}
+              key={key}
+              rowIndex={index}
+              parent={parent}
+            >
+              {({ measure }) => (
+                <AccordionListCell
+                  hasCursor={this.isRowSelected(rows[index])}
+                  {...this.props}
+                  style={style}
+                  row={rows[index]}
+                  sections={sections}
+                  onChange={this.handleChange}
+                  searchText={this.state.searchText}
+                  onChangeSearchText={this.handleChangeSearchText}
+                  sectionIsExpanded={this.isSectionExpanded}
+                  canToggleSections={this.canToggleSections()}
+                  toggleSection={this.toggleSection}
+                />
+              )}
+            </CellMeasurer>
+          );
+        }}
+        onRowsRendered={({ startIndex, stopIndex }) => {
+          this._startIndex = startIndex;
+          this._stopIndex = stopIndex;
+
+          if (searchRowIndex < startIndex || searchRowIndex > stopIndex) {
+            this.handleSearchRemoval();
+          }
+        }}
+      />
     );
   }
 }
diff --git a/frontend/test/metabase-visual/notebook/notebook.cy.spec.js b/frontend/test/metabase-visual/notebook/notebook.cy.spec.js
index 956d194d764..afccbb431b3 100644
--- a/frontend/test/metabase-visual/notebook/notebook.cy.spec.js
+++ b/frontend/test/metabase-visual/notebook/notebook.cy.spec.js
@@ -1,4 +1,5 @@
-import { restore, popover } from "__support__/e2e/cypress";
+import _ from "underscore";
+import { restore, popover, openNotebookEditor } from "__support__/e2e/cypress";
 
 describe("visual tests > notebook > major UI elements", () => {
   const VIEWPORT_WIDTH = 2500;
@@ -103,6 +104,28 @@ describe("visual tests > notebook > Run buttons", () => {
   });
 });
 
+describe("visual tests > notebook", () => {
+  const VIEWPORT_WIDTH = 1200;
+  const VIEWPORT_HEIGHT = 600;
+
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+    cy.viewport(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
+
+    _.range(10).forEach(index => {
+      const name = `Sample Database ${index + 1}`;
+      cy.addH2SampleDatabase({ name });
+    });
+  });
+
+  it("data picker", () => {
+    openNotebookEditor();
+    cy.findByText("Sample Database");
+    cy.percySnapshot();
+  });
+});
+
 function selectFromDropdown(itemName) {
   return popover().findByText(itemName);
 }
-- 
GitLab