Browse Source

ref(list-box): Remove context usage (#69759)

Remove compact select specific context usage from list box to make it
easier to re-use.
ArthurKnaus 10 months ago
parent
commit
b8eb9682c5

+ 83 - 97
static/app/components/comboBox/index.tsx

@@ -1,4 +1,4 @@
-import {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import isPropValid from '@emotion/is-prop-valid';
 import {useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
@@ -7,7 +7,6 @@ import {Item, Section} from '@react-stately/collections';
 import {type ComboBoxStateOptions, useComboBoxState} from '@react-stately/combobox';
 import omit from 'lodash/omit';
 
-import {SelectFilterContext} from 'sentry/components/compactSelect/list';
 import {ListBox} from 'sentry/components/compactSelect/listBox';
 import {
   getDisabledOptions,
@@ -26,8 +25,6 @@ import mergeRefs from 'sentry/utils/mergeRefs';
 import type {FormSize} from 'sentry/utils/theme';
 import useOverlay from 'sentry/utils/useOverlay';
 
-import {SelectContext} from '../compactSelect/control';
-
 import type {
   ComboBoxOption,
   ComboBoxOptionOrSection,
@@ -43,6 +40,8 @@ interface ComboBoxProps<Value extends string>
   className?: string;
   disabled?: boolean;
   growingInput?: boolean;
+  hasSearch?: boolean;
+  hiddenOptions?: Set<string>;
   isLoading?: boolean;
   loadingMessage?: string;
   menuSize?: FormSize;
@@ -65,6 +64,8 @@ function ComboBox<Value extends string>({
   growingInput = false,
   onOpenChange,
   menuWidth,
+  hiddenOptions,
+  hasSearch,
   ...props
 }: ComboBoxProps<Value>) {
   const theme = useTheme();
@@ -117,8 +118,6 @@ function ComboBox<Value extends string>({
     }
   }, [state.isOpen]);
 
-  const selectContext = useContext(SelectContext);
-
   const {overlayProps, triggerProps} = useOverlay({
     type: 'listbox',
     isOpen: state.isOpen,
@@ -163,54 +162,50 @@ function ComboBox<Value extends string>({
   const InputComponent = growingInput ? StyledGrowingInput : StyledInput;
 
   return (
-    <SelectContext.Provider
-      value={{
-        ...selectContext,
-        overlayIsOpen: state.isOpen,
-      }}
-    >
-      <ControlWrapper className={className}>
-        {!state.isFocused && <InteractionStateLayer />}
-        <InputComponent
-          {...inputProps}
-          onClick={handleInputClick}
-          placeholder={placeholder}
-          onMouseUp={handleInputMouseUp}
-          onFocus={handleInputFocus}
-          ref={mergeRefs([inputRef, triggerProps.ref])}
-          size={size}
-        />
-        <StyledPositionWrapper
-          {...overlayProps}
-          zIndex={theme.zIndex?.tooltip}
-          visible={state.isOpen}
-        >
-          <StyledOverlay ref={popoverRef} width={menuWidth}>
-            {isLoading && (
-              <MenuHeader size={menuSize ?? size}>
-                <MenuTitle>{loadingMessage ?? t('Loading...')}</MenuTitle>
-                <MenuHeaderTrailingItems>
-                  {isLoading && <StyledLoadingIndicator size={12} mini />}
-                </MenuHeaderTrailingItems>
-              </MenuHeader>
-            )}
-            {/* Listbox adds a separator if it is not the first item
+    <ControlWrapper className={className}>
+      {!state.isFocused && <InteractionStateLayer />}
+      <InputComponent
+        {...inputProps}
+        onClick={handleInputClick}
+        placeholder={placeholder}
+        onMouseUp={handleInputMouseUp}
+        onFocus={handleInputFocus}
+        ref={mergeRefs([inputRef, triggerProps.ref])}
+        size={size}
+      />
+      <StyledPositionWrapper
+        {...overlayProps}
+        zIndex={theme.zIndex?.tooltip}
+        visible={state.isOpen}
+      >
+        <StyledOverlay ref={popoverRef} width={menuWidth}>
+          {isLoading && (
+            <MenuHeader size={menuSize ?? size}>
+              <MenuTitle>{loadingMessage ?? t('Loading...')}</MenuTitle>
+              <MenuHeaderTrailingItems>
+                {isLoading && <StyledLoadingIndicator size={12} mini />}
+              </MenuHeaderTrailingItems>
+            </MenuHeader>
+          )}
+          {/* Listbox adds a separator if it is not the first item
             To avoid this, we wrap it into a div */}
-            <div>
-              <ListBox
-                {...listBoxProps}
-                ref={listBoxRef}
-                listState={state}
-                keyDownHandler={() => true}
-                size={menuSize ?? size}
-                sizeLimitMessage={sizeLimitMessage}
-              />
-              <EmptyMessage>No items found</EmptyMessage>
-            </div>
-          </StyledOverlay>
-        </StyledPositionWrapper>
-      </ControlWrapper>
-    </SelectContext.Provider>
+          <div>
+            <ListBox
+              {...listBoxProps}
+              overlayIsOpen={state.isOpen}
+              hiddenOptions={hiddenOptions}
+              hasSearch={hasSearch}
+              ref={listBoxRef}
+              listState={state}
+              keyDownHandler={() => true}
+              size={menuSize ?? size}
+              sizeLimitMessage={sizeLimitMessage}
+            />
+            <EmptyMessage>No items found</EmptyMessage>
+          </div>
+        </StyledOverlay>
+      </StyledPositionWrapper>
+    </ControlWrapper>
   );
 }
 
@@ -226,7 +221,10 @@ function ControlledComboBox<Value extends string>({
   value,
   onOpenChange,
   ...props
-}: Omit<ComboBoxProps<Value>, 'items' | 'defaultItems' | 'children'> & {
+}: Omit<
+  ComboBoxProps<Value>,
+  'items' | 'defaultItems' | 'children' | 'hasSearch' | 'hiddenOptions'
+> & {
   options: ComboBoxOptionOrSection<Value>[];
   defaultValue?: Value;
   onChange?: (value: ComboBoxOption<Value>) => void;
@@ -313,50 +311,38 @@ function ControlledComboBox<Value extends string>({
   );
 
   return (
-    // TODO: remove usage of SelectContext in ListBox
-    <SelectContext.Provider
-      value={{
-        search: isFiltering ? inputValue : '',
-        // Will be set by the inner ComboBox
-        overlayIsOpen: false,
-        // Not used in ComboBox
-        registerListState: () => {},
-        saveSelectedOptions: () => {},
-      }}
+    <ComboBox
+      disabledKeys={disabledKeys}
+      inputValue={inputValue}
+      onInputChange={handleInputChange}
+      selectedKey={value && getEscapedKey(value)}
+      defaultSelectedKey={props.defaultValue && getEscapedKey(props.defaultValue)}
+      onSelectionChange={handleChange}
+      items={items}
+      onOpenChange={handleOpenChange}
+      hasSearch={isFiltering ? !!inputValue : false}
+      hiddenOptions={hiddenOptions}
+      {...props}
     >
-      <SelectFilterContext.Provider value={hiddenOptions}>
-        <ComboBox
-          disabledKeys={disabledKeys}
-          inputValue={inputValue}
-          onInputChange={handleInputChange}
-          selectedKey={value && getEscapedKey(value)}
-          defaultSelectedKey={props.defaultValue && getEscapedKey(props.defaultValue)}
-          onSelectionChange={handleChange}
-          items={items}
-          onOpenChange={handleOpenChange}
-          {...props}
-        >
-          {items.map(item => {
-            if ('options' in item) {
-              return (
-                <Section key={item.key} title={item.label}>
-                  {item.options.map(option => (
-                    <Item {...option} key={option.key} textValue={option.label}>
-                      {item.label}
-                    </Item>
-                  ))}
-                </Section>
-              );
-            }
-            return (
-              <Item {...item} key={item.key} textValue={item.label}>
-                {item.label}
-              </Item>
-            );
-          })}
-        </ComboBox>
-      </SelectFilterContext.Provider>
-    </SelectContext.Provider>
+      {items.map(item => {
+        if ('options' in item) {
+          return (
+            <Section key={item.key} title={item.label}>
+              {item.options.map(option => (
+                <Item {...option} key={option.key} textValue={option.label}>
+                  {item.label}
+                </Item>
+              ))}
+            </Section>
+          );
+        }
+        return (
+          <Item {...item} key={item.key} textValue={item.label}>
+            {item.label}
+          </Item>
+        );
+      })}
+    </ComboBox>
   );
 }
 

+ 23 - 11
static/app/components/compactSelect/list.tsx

@@ -1,4 +1,11 @@
-import {createContext, useCallback, useContext, useLayoutEffect, useMemo} from 'react';
+import {
+  createContext,
+  Fragment,
+  useCallback,
+  useContext,
+  useLayoutEffect,
+  useMemo,
+} from 'react';
 import {useFocusManager} from '@react-aria/focus';
 import type {AriaGridListOptions} from '@react-aria/gridlist';
 import type {AriaListBoxOptions} from '@react-aria/listbox';
@@ -138,7 +145,7 @@ function List<Value extends SelectKey>({
   closeOnSelect,
   ...props
 }: SingleListProps<Value> | MultipleListProps<Value>) {
-  const {overlayState, registerListState, saveSelectedOptions, search} =
+  const {overlayState, registerListState, saveSelectedOptions, search, overlayIsOpen} =
     useContext(SelectContext);
 
   const hiddenOptions = useMemo(
@@ -345,18 +352,23 @@ function List<Value extends SelectKey>({
   );
 
   return (
-    <SelectFilterContext.Provider value={hiddenOptions}>
+    <Fragment>
       {grid ? (
-        <GridList
-          {...props}
-          id={listId}
-          listState={listState}
-          sizeLimitMessage={sizeLimitMessage}
-          keyDownHandler={keyDownHandler}
-        />
+        <SelectFilterContext.Provider value={hiddenOptions}>
+          <GridList
+            {...props}
+            id={listId}
+            listState={listState}
+            sizeLimitMessage={sizeLimitMessage}
+            keyDownHandler={keyDownHandler}
+          />
+        </SelectFilterContext.Provider>
       ) : (
         <ListBox
           {...props}
+          hasSearch={!!search}
+          overlayIsOpen={overlayIsOpen}
+          hiddenOptions={hiddenOptions}
           id={listId}
           listState={listState}
           shouldFocusWrap={shouldFocusWrap}
@@ -379,7 +391,7 @@ function List<Value extends SelectKey>({
               />
             )
         )}
-    </SelectFilterContext.Provider>
+    </Fragment>
   );
 }
 

+ 23 - 6
static/app/components/compactSelect/listBox/index.tsx

@@ -1,4 +1,4 @@
-import {forwardRef, Fragment, useCallback, useContext, useMemo, useRef} from 'react';
+import {forwardRef, Fragment, useCallback, useMemo, useRef} from 'react';
 import type {AriaListBoxOptions} from '@react-aria/listbox';
 import {useListBox} from '@react-aria/listbox';
 import {mergeProps} from '@react-aria/utils';
@@ -9,8 +9,6 @@ import {t} from 'sentry/locale';
 import mergeRefs from 'sentry/utils/mergeRefs';
 import type {FormSize} from 'sentry/utils/theme';
 
-import {SelectContext} from '../control';
-import {SelectFilterContext} from '../list';
 import {ListLabel, ListSeparator, ListWrap, SizeLimitMessage} from '../styles';
 import type {SelectKey, SelectSection} from '../types';
 
@@ -45,6 +43,15 @@ interface ListBoxProps
    */
   listState: ListState<any>;
   children?: CollectionChildren<any>;
+  /**
+   * Whether the list is filtered by search query or not.
+   * Used to determine whether to show the size limit message or not.
+   */
+  hasSearch?: boolean;
+  /**
+   * Set of keys that are hidden from the user (e.g. because not matching search query)
+   */
+  hiddenOptions?: Set<SelectKey>;
   /**
    * Text label to be rendered as heading on top of grid list.
    */
@@ -58,6 +65,13 @@ interface ListBoxProps
     section: SelectSection<SelectKey>,
     type: 'select' | 'unselect'
   ) => void;
+  /**
+   * Used to determine whether to render the list box items or not
+   */
+  overlayIsOpen?: boolean;
+  /**
+   * Size of the list box and its items.
+   */
   size?: FormSize;
   /**
    * Message to be displayed when some options are hidden due to `sizeLimit`.
@@ -65,6 +79,8 @@ interface ListBoxProps
   sizeLimitMessage?: string;
 }
 
+const EMPTY_SET = new Set<never>();
+
 /**
  * A list box with accessibile behaviors & attributes.
  * https://react-spectrum.adobe.com/react-aria/useListBox.html
@@ -85,6 +101,9 @@ const ListBox = forwardRef<HTMLUListElement, ListBoxProps>(function ListBox(
     sizeLimitMessage,
     keyDownHandler,
     label,
+    hiddenOptions = EMPTY_SET,
+    hasSearch,
+    overlayIsOpen,
     ...props
   }: ListBoxProps,
   forwarderdRef
@@ -111,8 +130,6 @@ const ListBox = forwardRef<HTMLUListElement, ListBoxProps>(function ListBox(
     [keyDownHandler, listBoxProps]
   );
 
-  const {overlayIsOpen, search} = useContext(SelectContext);
-  const hiddenOptions = useContext(SelectFilterContext);
   const listItems = useMemo(
     () =>
       [...listState.collection].filter(node => {
@@ -160,7 +177,7 @@ const ListBox = forwardRef<HTMLUListElement, ListBoxProps>(function ListBox(
             );
           })}
 
-        {!search && hiddenOptions.size > 0 && (
+        {!hasSearch && hiddenOptions.size > 0 && (
           <SizeLimitMessage>
             {sizeLimitMessage ?? t('Use search to find more options…')}
           </SizeLimitMessage>

+ 28 - 41
static/app/components/searchQueryBuilder/combobox.tsx

@@ -6,8 +6,6 @@ import {useComboBox} from '@react-aria/combobox';
 import {useComboBoxState} from '@react-stately/combobox';
 import type {CollectionChildren} from '@react-types/shared';
 
-import {SelectContext} from 'sentry/components/compactSelect/control';
-import {SelectFilterContext} from 'sentry/components/compactSelect/list';
 import {ListBox} from 'sentry/components/compactSelect/listBox';
 import type {SelectOptionWithKey} from 'sentry/components/compactSelect/types';
 import {
@@ -153,48 +151,37 @@ export function SearchQueryBuilderCombobox({
     [inputProps, state]
   );
 
-  const selectContextValue = useMemo(
-    () => ({
-      search: filterValue,
-      overlayIsOpen: isOpen,
-      registerListState: () => {},
-      saveSelectedOptions: () => {},
-    }),
-    [filterValue, isOpen]
-  );
-
   return (
-    <SelectContext.Provider value={selectContextValue}>
-      <SelectFilterContext.Provider value={hiddenOptions}>
-        <Wrapper>
-          <UnstyledInput
-            {...inputProps}
+    <Wrapper>
+      <UnstyledInput
+        {...inputProps}
+        size="md"
+        ref={mergeRefs([inputRef, triggerProps.ref])}
+        type="text"
+        placeholder={placeholder}
+        onClick={handleInputClick}
+        value={inputValue}
+        onChange={onInputChange}
+      />
+      <StyledPositionWrapper
+        {...overlayProps}
+        zIndex={theme.zIndex?.tooltip}
+        visible={isOpen}
+      >
+        <Overlay ref={popoverRef}>
+          <ListBox
+            {...listBoxProps}
+            ref={listBoxRef}
+            listState={state}
+            hasSearch={!!filterValue}
+            hiddenOptions={hiddenOptions}
+            keyDownHandler={() => true}
+            overlayIsOpen={isOpen}
             size="md"
-            ref={mergeRefs([inputRef, triggerProps.ref])}
-            type="text"
-            placeholder={placeholder}
-            onClick={handleInputClick}
-            value={inputValue}
-            onChange={onInputChange}
           />
-          <StyledPositionWrapper
-            {...overlayProps}
-            zIndex={theme.zIndex?.tooltip}
-            visible={isOpen}
-          >
-            <Overlay ref={popoverRef}>
-              <ListBox
-                {...listBoxProps}
-                ref={listBoxRef}
-                listState={state}
-                keyDownHandler={() => true}
-                size="md"
-              />
-            </Overlay>
-          </StyledPositionWrapper>
-        </Wrapper>
-      </SelectFilterContext.Provider>
-    </SelectContext.Provider>
+        </Overlay>
+      </StyledPositionWrapper>
+    </Wrapper>
   );
 }