Browse Source

ref(js): Simplify BreadcrumbDropdown (#40165)

Evan Purkhiser 2 years ago
parent
commit
44fd618eb9

+ 1 - 5
static/app/components/breadcrumbs.spec.jsx

@@ -84,10 +84,6 @@ describe('Breadcrumbs', () => {
     expect(item3).toBeInTheDocument();
 
     userEvent.click(item3);
-    expect(onSelect).toHaveBeenCalledWith(
-      expect.objectContaining({label: 'item3'}),
-      expect.anything(),
-      expect.anything()
-    );
+    expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({label: 'item3'}));
   });
 });

+ 2 - 20
static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.spec.jsx

@@ -2,8 +2,6 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
 import BreadcrumbDropdown from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbDropdown';
 
-jest.useFakeTimers();
-
 describe('Settings Breadcrumb Dropdown', () => {
   const selectMock = jest.fn();
   const items = [
@@ -21,55 +19,39 @@ describe('Settings Breadcrumb Dropdown', () => {
     createWrapper();
     expect(screen.getByText('Test')).toBeInTheDocument();
     userEvent.hover(screen.getByText('Test'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
     expect(screen.getByText('bar')).toBeInTheDocument();
   });
 
-  it('closes after 200ms when mouse leaves crumb', () => {
-    createWrapper();
-    userEvent.hover(screen.getByText('Test'));
-    jest.runAllTimers();
-    expect(screen.getByText('foo')).toBeInTheDocument();
-
-    userEvent.unhover(screen.getByText('Test'));
-    jest.advanceTimersByTime(10);
-    expect(screen.queryByText('foo')).not.toBeInTheDocument();
-  });
-
   it('closes immediately after selecting an item', () => {
     createWrapper();
     userEvent.hover(screen.getByText('Test'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
 
     userEvent.click(screen.getByText('foo'));
+    expect(selectMock).toHaveBeenCalled();
+
     expect(screen.queryByText('foo')).not.toBeInTheDocument();
   });
 
   it('stays open when hovered over crumb and then into dropdown menu', () => {
     createWrapper();
     userEvent.hover(screen.getByText('Test'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
 
     userEvent.hover(screen.getByText('foo'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
   });
 
   it('closes after entering dropdown and then leaving dropdown', () => {
     createWrapper();
     userEvent.hover(screen.getByText('Test'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
 
     userEvent.hover(screen.getByText('foo'));
-    jest.runAllTimers();
     expect(screen.getByText('foo')).toBeInTheDocument();
     userEvent.unhover(screen.getByText('foo'));
 
-    jest.runAllTimers();
     expect(screen.queryByText('foo')).not.toBeInTheDocument();
   });
 });

+ 40 - 99
static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.tsx

@@ -1,4 +1,4 @@
-import {Component} from 'react';
+import {useState} from 'react';
 
 import DropdownAutoCompleteMenu from 'sentry/components/dropdownAutoComplete/menu';
 import {Item} from 'sentry/components/dropdownAutoComplete/types';
@@ -7,8 +7,6 @@ import Divider from 'sentry/views/settings/components/settingsBreadcrumb/divider
 
 import {RouteWithName} from './types';
 
-const EXIT_DELAY = 0;
-
 interface AdditionalDropdownProps
   extends Pick<
     React.ComponentProps<typeof DropdownAutoCompleteMenu>,
@@ -20,106 +18,49 @@ export interface BreadcrumbDropdownProps extends AdditionalDropdownProps {
   name: React.ReactNode;
   onSelect: (item: Item) => void;
   route: RouteWithName;
-  enterDelay?: number;
   hasMenu?: boolean;
   isLast?: boolean;
 }
 
-type State = {
-  isOpen: boolean;
-};
-
-class BreadcrumbDropdown extends Component<BreadcrumbDropdownProps, State> {
-  state: State = {
-    isOpen: false,
-  };
-
-  componentWillUnmount() {
-    window.clearTimeout(this.enteringTimeout);
-    window.clearTimeout(this.leavingTimeout);
-  }
-
-  enteringTimeout: number | undefined = undefined;
-  leavingTimeout: number | undefined = undefined;
-
-  open = () => {
-    this.setState({isOpen: true});
-  };
-
-  close = () => {
-    this.setState({isOpen: false});
-  };
-
-  handleStateChange = () => {};
-
-  // Adds a delay when mouse hovers on actor (in this case the breadcrumb)
-  handleMouseEnterActor = () => {
-    window.clearTimeout(this.leavingTimeout);
-    window.clearTimeout(this.enteringTimeout);
-
-    this.enteringTimeout = window.setTimeout(
-      () => this.open(),
-      this.props.enterDelay ?? 0
-    );
-  };
-
-  // handles mouseEnter event on actor and menu, should clear the leaving timeout and keep menu open
-  handleMouseEnter = () => {
-    window.clearTimeout(this.leavingTimeout);
-    window.clearTimeout(this.enteringTimeout);
-    this.open();
-  };
-
-  // handles mouseLeave event on actor and menu, adds a timeout before updating state to account for
-  // mouseLeave into
-  handleMouseLeave = () => {
-    window.clearTimeout(this.enteringTimeout);
-    this.leavingTimeout = window.setTimeout(() => this.close(), EXIT_DELAY);
-  };
-
-  // Close immediately when actor is clicked clicked
-  handleClickActor = () => {
-    this.close();
-  };
-
-  // Close immediately when clicked outside
-  handleClose = () => {
-    this.close();
-  };
-
-  render() {
-    const {hasMenu, route, isLast, name, items, onSelect, ...dropdownProps} = this.props;
-
-    return (
-      <DropdownAutoCompleteMenu
-        blendCorner={false}
-        onOpen={this.handleMouseEnter}
-        onClose={this.close}
-        isOpen={this.state.isOpen}
-        menuProps={{
-          onMouseEnter: this.handleMouseEnter,
-          onMouseLeave: this.handleMouseLeave,
-        }}
-        items={items}
-        onSelect={onSelect}
-        virtualizedHeight={41}
-        {...dropdownProps}
-      >
-        {({getActorProps, actions, isOpen}) => (
-          <Crumb
-            {...getActorProps({
-              onClick: this.handleClickActor.bind(this, actions),
-              onMouseEnter: this.handleMouseEnterActor.bind(this, actions),
-              onMouseLeave: this.handleMouseLeave.bind(this, actions),
-            })}
-          >
-            <span>{name || route.name} </span>
-            <Divider isHover={hasMenu && isOpen} isLast={isLast} />
-          </Crumb>
-        )}
-      </DropdownAutoCompleteMenu>
-    );
-  }
+function BreadcrumbDropdown({
+  hasMenu,
+  route,
+  isLast,
+  name,
+  onSelect,
+  ...dropdownProps
+}: BreadcrumbDropdownProps) {
+  const [isActive, setIsActive] = useState(false);
+
+  return (
+    <DropdownAutoCompleteMenu
+      blendCorner={false}
+      isOpen={isActive}
+      virtualizedHeight={41}
+      onSelect={item => {
+        setIsActive(false);
+        onSelect(item);
+      }}
+      menuProps={{
+        onMouseEnter: () => setIsActive(true),
+        onMouseLeave: () => setIsActive(false),
+      }}
+      {...dropdownProps}
+    >
+      {({getActorProps, isOpen}) => (
+        <Crumb
+          {...getActorProps({
+            onClick: () => setIsActive(false),
+            onMouseEnter: () => setIsActive(true),
+            onMouseLeave: () => setIsActive(false),
+          })}
+        >
+          <span>{name || route.name} </span>
+          <Divider isHover={hasMenu && isOpen} isLast={isLast} />
+        </Crumb>
+      )}
+    </DropdownAutoCompleteMenu>
+  );
 }
 
 export default BreadcrumbDropdown;