Browse Source

feat(compactSelect): Add optional menu footer (#45339)

For example:
<img width="295" alt="Screenshot 2023-03-02 at 1 23 33 PM"
src="https://user-images.githubusercontent.com/44172267/222557288-85a47cda-1e46-4d15-95ba-004f7e596ef7.png">
Vu Luong 2 years ago
parent
commit
1c6b3515e9

+ 12 - 0
static/app/components/compactSelect/composite.tsx

@@ -206,6 +206,18 @@ const RegionsWrap = styled('div')`
   overflow: auto;
   padding: ${space(0.5)} 0;
 
+  /* Add 1px to top padding if preceded by menu header, to account for the header's
+  shadow border */
+  [data-menu-has-header='true'] > div > & {
+    padding-top: calc(${space(0.5)} + 1px);
+  }
+
+  /* Add 1px to bottom padding if succeeded by menu footer, to account for the footer's
+  shadow border */
+  [data-menu-has-footer='true'] > div > & {
+    padding-bottom: calc(${space(0.5)} + 1px);
+  }
+
   /* Remove padding inside lists */
   > ul {
     padding: 0;

+ 24 - 2
static/app/components/compactSelect/control.tsx

@@ -88,6 +88,12 @@ export interface ControlProps extends UseOverlayProps {
   loading?: boolean;
   maxMenuHeight?: number | string;
   maxMenuWidth?: number | string;
+  /**
+   * Footer to be rendered at the bottom of the menu.
+   */
+  menuFooter?:
+    | React.ReactNode
+    | ((actions: {closeOverlay: () => void}) => React.ReactNode);
   /**
    * Title to display in the menu's header. Keep the title as short as possible.
    */
@@ -150,6 +156,7 @@ export function Control({
   maxMenuHeight = '32rem',
   maxMenuWidth,
   menuWidth,
+  menuFooter,
 
   // Select props
   size = 'md',
@@ -360,10 +367,12 @@ export function Control({
             maxWidth={maxMenuWidth}
             maxHeight={overlayProps.style.maxHeight}
             maxHeightProp={maxMenuHeight}
+            data-menu-has-header={!!menuTitle || clearable}
+            data-menu-has-footer={!!menuFooter}
           >
             <FocusScope contain={overlayIsOpen}>
               {(menuTitle || clearable) && (
-                <MenuHeader size={size} data-header>
+                <MenuHeader size={size}>
                   <MenuTitle>{menuTitle}</MenuTitle>
                   <MenuHeaderTrailingItems>
                     {loading && <StyledLoadingIndicator size={12} mini />}
@@ -385,6 +394,13 @@ export function Control({
                 />
               )}
               <OptionsWrap>{children}</OptionsWrap>
+              {menuFooter && (
+                <MenuFooter>
+                  {typeof menuFooter === 'function'
+                    ? menuFooter({closeOverlay: overlayState.close})
+                    : menuFooter}
+                </MenuFooter>
+              )}
             </FocusScope>
           </StyledOverlay>
         </StyledPositionWrapper>
@@ -477,7 +493,7 @@ const SearchInput = styled('input')<{visualSize: FormSize}>`
 
   /* Add 1px to top margin if immediately preceded by menu header, to account for the
   header's shadow border */
-  div[data-header] + & {
+  [data-menu-has-header='true'] > & {
     margin-top: calc(${space(0.5)} + 1px);
   }
 
@@ -526,3 +542,9 @@ const OptionsWrap = styled('div')`
   flex-direction: column;
   min-height: 0;
 `;
+
+const MenuFooter = styled('div')`
+  box-shadow: 0 -1px 0 ${p => p.theme.translucentInnerBorder};
+  padding: ${space(1)} ${space(1.5)};
+  z-index: 2;
+`;

+ 7 - 2
static/app/components/compactSelect/styles.tsx

@@ -13,11 +13,16 @@ export const ListWrap = styled('ul')`
 
   /* Add 1px to top padding if preceded by menu header, to account for the header's
   shadow border */
-  div[data-header] ~ &:first-of-type,
-  div[data-header] ~ div > &:first-of-type {
+  [data-menu-has-header='true'] > div > &:first-of-type {
     padding-top: calc(${space(0.5)} + 1px);
   }
 
+  /* Add 1px to bottom padding if succeeded by menu footer, to account for the footer's
+  shadow border */
+  [data-menu-has-footer='true'] > div > &:last-of-type {
+    padding-bottom: calc(${space(0.5)} + 1px);
+  }
+
   /* Remove top padding if preceded by search input, since search input already has
   vertical padding */
   input ~ &&:first-of-type,