Browse Source

ref(ts): Use more accurate generic renderProps for dropdownMenu (#25631)

Evan Purkhiser 3 years ago
parent
commit
ac97941e58

+ 36 - 29
static/app/components/autoComplete.tsx

@@ -31,19 +31,18 @@ const defaultProps = {
   shouldSelectWithTab: false,
 };
 
-type GetInputArgs = {
+type GetInputArgs<E extends HTMLInputElement> = {
   type?: string;
   placeholder?: string;
   style?: React.CSSProperties;
-  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
-  onKeyDown?: (event: React.KeyboardEvent) => void;
-  onFocus?: (event: React.FocusEvent) => void;
-  onBlur?: (event: React.FocusEvent) => void;
+  onChange?: (event: React.ChangeEvent<E>) => void;
+  onKeyDown?: (event: React.KeyboardEvent<E>) => void;
+  onFocus?: (event: React.FocusEvent<E>) => void;
+  onBlur?: (event: React.FocusEvent<E>) => void;
 };
 
-type GetInputOutput = GetInputArgs &
-  Omit<GetActorArgs, 'onChange'> & {
-    onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+type GetInputOutput<E extends HTMLInputElement> = GetInputArgs<E> &
+  GetActorArgs<E> & {
     value?: string;
   };
 
@@ -56,7 +55,9 @@ export type GetItemArgs<T> = {
 
 type ChildrenProps<T> = Parameters<DropdownMenu['props']['children']>[0] & {
   highlightedIndex: number;
-  getInputProps: (args: GetInputArgs) => GetInputOutput;
+  getInputProps: <E extends HTMLInputElement = HTMLInputElement>(
+    args: GetInputArgs<E>
+  ) => GetInputOutput<E>;
   getItemProps: (
     args: GetItemArgs<T>
   ) => Pick<GetItemArgs<T>, 'style'> & {
@@ -152,9 +153,9 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
     });
   };
 
-  handleInputChange = ({onChange}: Pick<GetInputArgs, 'onChange'>) => (
-    e: React.ChangeEvent<HTMLInputElement>
-  ) => {
+  handleInputChange = <E extends HTMLInputElement>({
+    onChange,
+  }: Pick<GetInputArgs<E>, 'onChange'>) => (e: React.ChangeEvent<E>) => {
     const value = e.target.value;
 
     // We force `isOpen: true` here because:
@@ -168,9 +169,9 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
     onChange?.(e);
   };
 
-  handleInputFocus = ({onFocus}: Pick<GetInputArgs, 'onFocus'>) => (
-    e: React.FocusEvent
-  ) => {
+  handleInputFocus = <E extends HTMLInputElement>({
+    onFocus,
+  }: Pick<GetInputArgs<E>, 'onFocus'>) => (e: React.FocusEvent<E>) => {
     this.openMenu();
     onFocus?.(e);
   };
@@ -184,7 +185,9 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
    * Clicks outside should close the dropdown immediately via <DropdownMenu />,
    * however blur via keyboard will have a 200ms delay
    */
-  handleInputBlur = ({onBlur}: Pick<GetInputArgs, 'onBlur'>) => (e: React.FocusEvent) => {
+  handleInputBlur = <E extends HTMLInputElement>({
+    onBlur,
+  }: Pick<GetInputArgs<E>, 'onBlur'>) => (e: React.FocusEvent<E>) => {
     this.blurTimer = setTimeout(() => {
       this.closeMenu();
       onBlur?.(e);
@@ -209,9 +212,9 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
     this.closeMenu();
   };
 
-  handleInputKeyDown = ({onKeyDown}: Pick<GetInputArgs, 'onKeyDown'>) => (
-    e: React.KeyboardEvent
-  ) => {
+  handleInputKeyDown = <E extends HTMLInputElement>({
+    onKeyDown,
+  }: Pick<GetInputArgs<E>, 'onKeyDown'>) => (e: React.KeyboardEvent<E>) => {
     const hasHighlightedItem =
       this.items.size && this.items.has(this.state.highlightedIndex);
     const canSelectWithEnter = this.props.shouldSelectWithEnter && e.key === 'Enter';
@@ -331,15 +334,17 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
     }));
   };
 
-  getInputProps = (inputProps?: GetInputArgs): GetInputOutput => {
+  getInputProps = <E extends HTMLInputElement>(
+    inputProps?: GetInputArgs<E>
+  ): GetInputOutput<E> => {
     const {onChange, onKeyDown, onFocus, onBlur, ...rest} = inputProps ?? {};
     return {
       ...rest,
       value: this.state.inputValue,
-      onChange: this.handleInputChange({onChange}),
-      onKeyDown: this.handleInputKeyDown({onKeyDown}),
-      onFocus: this.handleInputFocus({onFocus}),
-      onBlur: this.handleInputBlur({onBlur}),
+      onChange: this.handleInputChange<E>({onChange}),
+      onKeyDown: this.handleInputKeyDown<E>({onKeyDown}),
+      onFocus: this.handleInputFocus<E>({onFocus}),
+      onBlur: this.handleInputBlur<E>({onBlur}),
     };
   };
 
@@ -360,7 +365,7 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
     };
   };
 
-  getMenuProps = (props?: GetMenuArgs): GetMenuArgs => {
+  getMenuProps = <E extends Element>(props?: GetMenuArgs<E>): GetMenuArgs<E> => {
     this.itemCount = props?.itemCount;
 
     return {
@@ -383,16 +388,18 @@ class AutoComplete<T> extends React.Component<Props<T>, State<T>> {
         {dropdownMenuProps =>
           children({
             ...dropdownMenuProps,
-            getMenuProps: (props?: GetMenuArgs) =>
+            getMenuProps: <E extends Element = Element>(props?: GetMenuArgs<E>) =>
               dropdownMenuProps.getMenuProps(this.getMenuProps(props)),
-            getInputProps: (props?: GetInputArgs) => {
-              const inputProps = this.getInputProps(props);
+            getInputProps: <E extends HTMLInputElement = HTMLInputElement>(
+              props?: GetInputArgs<E>
+            ): GetInputOutput<E> => {
+              const inputProps = this.getInputProps<E>(props);
 
               if (!inputIsActor) {
                 return inputProps;
               }
 
-              return dropdownMenuProps.getActorProps(inputProps as GetActorArgs);
+              return dropdownMenuProps.getActorProps<E>(inputProps as GetActorArgs<E>);
             },
             getItemProps: this.getItemProps,
             inputValue,

+ 1 - 1
static/app/components/dropdownAutoComplete/index.tsx

@@ -15,7 +15,7 @@ const DropdownAutoComplete = ({allowActorToggle = false, children, ...props}: Pr
     {renderProps => {
       const {isOpen, actions, getActorProps} = renderProps;
       // Don't pass `onClick` from `getActorProps`
-      const {onClick: _onClick, ...actorProps} = getActorProps();
+      const {onClick: _onClick, ...actorProps} = getActorProps<HTMLDivElement>();
       return (
         <Actor
           isOpen={isOpen}

+ 41 - 36
static/app/components/dropdownMenu.tsx

@@ -3,45 +3,50 @@ import * as Sentry from '@sentry/react';
 
 import {MENU_CLOSE_DELAY} from 'app/constants';
 
-export type GetActorArgs = {
-  onClick?: (e: React.MouseEvent<Element>) => void;
-  onMouseEnter?: (e: React.MouseEvent<Element>) => void;
-  onMouseLeave?: (e: React.MouseEvent<Element>) => void;
-  onKeyDown?: (e: React.KeyboardEvent<Element>) => void;
-  onFocus?: (e: React.FocusEvent<Element>) => void;
-  onBlur?: (e: React.FocusEvent<Element>) => void;
-  onChange?: (e: React.ChangeEvent<Element>) => void;
+export type GetActorArgs<E extends Element> = {
+  onClick?: (e: React.MouseEvent<E>) => void;
+  onMouseEnter?: (e: React.MouseEvent<E>) => void;
+  onMouseLeave?: (e: React.MouseEvent<E>) => void;
+  onKeyDown?: (e: React.KeyboardEvent<E>) => void;
+  onFocus?: (e: React.FocusEvent<E>) => void;
+  onBlur?: (e: React.FocusEvent<E>) => void;
+  onChange?: (e: React.ChangeEvent<E>) => void;
   style?: React.CSSProperties;
   className?: string;
 };
 
-export type GetMenuArgs = {
-  onClick?: (e: React.MouseEvent<Element>) => void;
-  onMouseEnter?: (e: React.MouseEvent<Element>) => void;
-  onMouseLeave?: (e: React.MouseEvent<Element>) => void;
-  onMouseDown?: (e: React.MouseEvent<Element>) => void;
-  onKeyDown?: (event: React.KeyboardEvent<Element>) => void;
+export type GetMenuArgs<E extends Element> = {
+  onClick?: (e: React.MouseEvent<E>) => void;
+  onMouseEnter?: (e: React.MouseEvent<E>) => void;
+  onMouseLeave?: (e: React.MouseEvent<E>) => void;
+  onMouseDown?: (e: React.MouseEvent<E>) => void;
+  onKeyDown?: (event: React.KeyboardEvent<E>) => void;
   className?: string;
   itemCount?: number;
 };
 
 // Props for the "actor" element of `<DropdownMenu>`
 // This is the element that handles visibility of the dropdown menu
-type ActorProps = {
-  onClick: (e: React.MouseEvent<Element>) => void;
-  onMouseEnter: (e: React.MouseEvent<Element>) => void;
-  onMouseLeave: (e: React.MouseEvent<Element>) => void;
-  onKeyDown: (e: React.KeyboardEvent<Element>) => void;
+type ActorProps<E extends Element> = {
+  onClick: (e: React.MouseEvent<E>) => void;
+  onMouseEnter: (e: React.MouseEvent<E>) => void;
+  onMouseLeave: (e: React.MouseEvent<E>) => void;
+  onKeyDown: (e: React.KeyboardEvent<E>) => void;
 };
 
-type MenuProps = {
-  onClick: (e: React.MouseEvent<Element>) => void;
-  onMouseEnter: (e: React.MouseEvent<Element>) => void;
-  onMouseLeave: (e: React.MouseEvent<Element>) => void;
+type MenuProps<E extends Element> = {
+  onClick: (e: React.MouseEvent<E>) => void;
+  onMouseEnter: (e: React.MouseEvent<E>) => void;
+  onMouseLeave: (e: React.MouseEvent<E>) => void;
 };
 
-export type GetActorPropsFn = (opts?: GetActorArgs) => ActorProps;
-export type GetMenuPropsFn = (opts?: GetMenuArgs) => MenuProps;
+export type GetActorPropsFn = <E extends Element = Element>(
+  opts?: GetActorArgs<E>
+) => ActorProps<E>;
+
+export type GetMenuPropsFn = <E extends Element = Element>(
+  opts?: GetMenuArgs<E>
+) => MenuProps<E>;
 
 type RenderProps = {
   isOpen: boolean;
@@ -306,14 +311,14 @@ class DropdownMenu extends React.Component<Props, State> {
   }
 
   // Actor is the component that will open the dropdown menu
-  getActorProps: GetActorPropsFn = ({
+  getActorProps: GetActorPropsFn = <E extends Element = Element>({
     onClick,
     onMouseEnter,
     onMouseLeave,
     onKeyDown,
     style = {},
     ...props
-  }: GetActorArgs = {}) => {
+  }: GetActorArgs<E> = {}) => {
     const {isNestedDropdown, closeOnEscape} = this.props;
 
     const refProps = {ref: this.handleActorMount};
@@ -324,7 +329,7 @@ class DropdownMenu extends React.Component<Props, State> {
       ...refProps,
       style: {...style, outline: 'none'},
 
-      onKeyDown: (e: React.KeyboardEvent<Element>) => {
+      onKeyDown: (e: React.KeyboardEvent<E>) => {
         if (typeof onKeyDown === 'function') {
           onKeyDown(e);
         }
@@ -334,7 +339,7 @@ class DropdownMenu extends React.Component<Props, State> {
         }
       },
 
-      onMouseEnter: (e: React.MouseEvent<Element>) => {
+      onMouseEnter: (e: React.MouseEvent<E>) => {
         if (typeof onMouseEnter === 'function') {
           onMouseEnter(e);
         }
@@ -353,7 +358,7 @@ class DropdownMenu extends React.Component<Props, State> {
         }, MENU_CLOSE_DELAY);
       },
 
-      onMouseLeave: (e: React.MouseEvent<Element>) => {
+      onMouseLeave: (e: React.MouseEvent<E>) => {
         if (typeof onMouseLeave === 'function') {
           onMouseLeave(e);
         }
@@ -364,7 +369,7 @@ class DropdownMenu extends React.Component<Props, State> {
         this.handleMouseLeave(e);
       },
 
-      onClick: (e: React.MouseEvent<Element>) => {
+      onClick: (e: React.MouseEvent<E>) => {
         // If we are a nested dropdown, clicking the actor
         // should be a no-op so that the menu doesn't close.
         if (isNestedDropdown) {
@@ -383,19 +388,19 @@ class DropdownMenu extends React.Component<Props, State> {
   };
 
   // Menu is the menu component that <DropdownMenu> will control
-  getMenuProps: GetMenuPropsFn = ({
+  getMenuProps: GetMenuPropsFn = <E extends Element = Element>({
     onClick,
     onMouseLeave,
     onMouseEnter,
     ...props
-  }: GetMenuArgs = {}): MenuProps => {
+  }: GetMenuArgs<E> = {}): MenuProps<E> => {
     const refProps = {ref: this.handleMenuMount};
 
     // Props that the menu needs to have <DropdownMenu> work
     return {
       ...props,
       ...refProps,
-      onMouseEnter: (e: React.MouseEvent<Element>) => {
+      onMouseEnter: (e: React.MouseEvent<E>) => {
         if (typeof onMouseEnter === 'function') {
           onMouseEnter(e);
         }
@@ -405,14 +410,14 @@ class DropdownMenu extends React.Component<Props, State> {
           window.clearTimeout(this.mouseLeaveId);
         }
       },
-      onMouseLeave: (e: React.MouseEvent<Element>) => {
+      onMouseLeave: (e: React.MouseEvent<E>) => {
         if (typeof onMouseLeave === 'function') {
           onMouseLeave(e);
         }
 
         this.handleMouseLeave(e);
       },
-      onClick: (e: React.MouseEvent<Element>) => {
+      onClick: (e: React.MouseEvent<E>) => {
         this.handleDropdownMenuClick(e);
 
         if (typeof onClick === 'function') {

+ 1 - 1
static/app/views/dashboardsV2/widgetCard.tsx

@@ -295,7 +295,7 @@ const ContextMenu = ({children}) => (
           })}
         >
           <DropdownTarget
-            {...getActorProps({
+            {...getActorProps<HTMLDivElement>({
               onClick: (event: MouseEvent) => {
                 event.stopPropagation();
                 event.preventDefault();

+ 1 - 1
static/app/views/eventsV2/queryList.tsx

@@ -287,7 +287,7 @@ const ContextMenu = ({children}) => (
           })}
         >
           <DropdownTarget
-            {...getActorProps({
+            {...getActorProps<HTMLDivElement>({
               onClick: (event: MouseEvent) => {
                 event.stopPropagation();
                 event.preventDefault();