Browse Source

ref(js): Clean up Collapsible component (#25592)

My main intent here was to avoid the DOM reference of the Button from
changing when expanding / collapsing, specifically for use in the
sidebar, where it will close if a clicked element is removed from the
DOM.
Evan Purkhiser 3 years ago
parent
commit
cf6e51d058

+ 41 - 74
static/app/components/collapsible.tsx

@@ -9,91 +9,58 @@ type CollapseButtonRenderProps = {
 
 
 type ExpandButtonRenderProps = {
 type ExpandButtonRenderProps = {
   onExpand: () => void;
   onExpand: () => void;
-  numberOfCollapsedItems: number;
-};
-
-type DefaultProps = {
-  maxVisibleItems: number;
+  numberOfHiddenItems: number;
 };
 };
 
 
 type Props = {
 type Props = {
+  maxVisibleItems?: number;
   collapseButton?: (props: CollapseButtonRenderProps) => React.ReactNode;
   collapseButton?: (props: CollapseButtonRenderProps) => React.ReactNode;
   expandButton?: (props: ExpandButtonRenderProps) => React.ReactNode;
   expandButton?: (props: ExpandButtonRenderProps) => React.ReactNode;
-} & DefaultProps;
-
-type State = {
-  collapsed: boolean;
 };
 };
 
 
 /**
 /**
  * This component is used to show first X items and collapse the rest
  * This component is used to show first X items and collapse the rest
  */
  */
-class Collapsible extends React.Component<Props, State> {
-  static defaultProps: DefaultProps = {
-    maxVisibleItems: 5,
-  };
-
-  state: State = {
-    collapsed: true,
-  };
-
-  handleCollapseToggle = () => {
-    this.setState(prevState => ({
-      collapsed: !prevState.collapsed,
-    }));
-  };
-
-  renderCollapseButton() {
-    const {collapseButton} = this.props;
-
-    if (typeof collapseButton === 'function') {
-      return collapseButton({onCollapse: this.handleCollapseToggle});
-    }
-
-    return (
-      <Button priority="link" onClick={this.handleCollapseToggle}>
-        {t('Collapse')}
-      </Button>
-    );
-  }
-
-  renderExpandButton(numberOfCollapsedItems: number) {
-    const {expandButton} = this.props;
-
-    if (typeof expandButton === 'function') {
-      return expandButton({
-        onExpand: this.handleCollapseToggle,
-        numberOfCollapsedItems,
-      });
-    }
-
-    return (
-      <Button priority="link" onClick={this.handleCollapseToggle}>
-        {tn('Show %s collapsed item', 'Show %s collapsed items', numberOfCollapsedItems)}
-      </Button>
-    );
+const Collapsible: React.FC<Props> = ({
+  collapseButton,
+  expandButton,
+  maxVisibleItems = 5,
+  children,
+}) => {
+  const [isCollapsed, setCollapsed] = React.useState(true);
+  const handleCollapseToggle = () => setCollapsed(!isCollapsed);
+
+  const items = React.Children.toArray(children);
+  const canCollapse = items.length > maxVisibleItems;
+
+  if (!canCollapse) {
+    return <React.Fragment>{children}</React.Fragment>;
   }
   }
 
 
-  render() {
-    const {maxVisibleItems, children} = this.props;
-    const {collapsed} = this.state;
-
-    const items = React.Children.toArray(children);
-    const canExpand = items.length > maxVisibleItems;
-    const itemsToRender =
-      collapsed && canExpand ? items.slice(0, maxVisibleItems) : items;
-    const numberOfCollapsedItems = items.length - itemsToRender.length;
-
-    return (
-      <React.Fragment>
-        {itemsToRender}
-
-        {numberOfCollapsedItems > 0 && this.renderExpandButton(numberOfCollapsedItems)}
-
-        {numberOfCollapsedItems === 0 && canExpand && this.renderCollapseButton()}
-      </React.Fragment>
-    );
-  }
-}
+  const visibleItems = isCollapsed ? items.slice(0, maxVisibleItems) : items;
+  const numberOfHiddenItems = items.length - visibleItems.length;
+
+  const showDefault =
+    (numberOfHiddenItems > 0 && !expandButton) ||
+    (numberOfHiddenItems === 0 && !collapseButton);
+
+  return (
+    <React.Fragment>
+      {visibleItems}
+
+      {showDefault && (
+        <Button priority="link" onClick={handleCollapseToggle}>
+          {isCollapsed
+            ? tn('Show %s hidden item', 'Show %s hidden items', numberOfHiddenItems)
+            : t('Collapse')}
+        </Button>
+      )}
+
+      {numberOfHiddenItems > 0 &&
+        expandButton?.({onExpand: handleCollapseToggle, numberOfHiddenItems})}
+      {numberOfHiddenItems === 0 && collapseButton?.({onCollapse: handleCollapseToggle})}
+    </React.Fragment>
+  );
+};
 
 
 export default Collapsible;
 export default Collapsible;

+ 2 - 6
static/app/views/projectDetail/projectTeamAccess.tsx

@@ -46,13 +46,9 @@ function ProjectTeamAccess({organization, project}: Props) {
 
 
     return (
     return (
       <Collapsible
       <Collapsible
-        expandButton={({onExpand, numberOfCollapsedItems}) => (
+        expandButton={({onExpand, numberOfHiddenItems}) => (
           <Button priority="link" onClick={onExpand}>
           <Button priority="link" onClick={onExpand}>
-            {tn(
-              'Show %s collapsed team',
-              'Show %s collapsed teams',
-              numberOfCollapsedItems
-            )}
+            {tn('Show %s collapsed team', 'Show %s collapsed teams', numberOfHiddenItems)}
           </Button>
           </Button>
         )}
         )}
       >
       >

+ 2 - 2
static/app/views/releases/detail/overview/commitAuthorBreakdown.tsx

@@ -80,12 +80,12 @@ class CommitAuthorBreakdown extends AsyncComponent<Props, State> {
       <Wrapper>
       <Wrapper>
         <SectionHeading>{t('Commit Author Breakdown')}</SectionHeading>
         <SectionHeading>{t('Commit Author Breakdown')}</SectionHeading>
         <Collapsible
         <Collapsible
-          expandButton={({onExpand, numberOfCollapsedItems}) => (
+          expandButton={({onExpand, numberOfHiddenItems}) => (
             <Button priority="link" onClick={onExpand}>
             <Button priority="link" onClick={onExpand}>
               {tn(
               {tn(
                 'Show %s collapsed author',
                 'Show %s collapsed author',
                 'Show %s collapsed authors',
                 'Show %s collapsed authors',
-                numberOfCollapsedItems
+                numberOfHiddenItems
               )}
               )}
             </Button>
             </Button>
           )}
           )}

+ 2 - 2
static/app/views/releases/detail/overview/otherProjects.tsx

@@ -32,12 +32,12 @@ function OtherProjects({projects, location, version, organization}: Props) {
       </SectionHeading>
       </SectionHeading>
 
 
       <Collapsible
       <Collapsible
-        expandButton={({onExpand, numberOfCollapsedItems}) => (
+        expandButton={({onExpand, numberOfHiddenItems}) => (
           <Button priority="link" onClick={onExpand}>
           <Button priority="link" onClick={onExpand}>
             {tn(
             {tn(
               'Show %s collapsed project',
               'Show %s collapsed project',
               'Show %s collapsed projects',
               'Show %s collapsed projects',
-              numberOfCollapsedItems
+              numberOfHiddenItems
             )}
             )}
           </Button>
           </Button>
         )}
         )}

+ 2 - 4
static/app/views/releases/list/releaseHealth/content.tsx

@@ -77,12 +77,10 @@ const Content = ({
 
 
       <ProjectRows>
       <ProjectRows>
         <Collapsible
         <Collapsible
-          expandButton={({onExpand, numberOfCollapsedItems}) => (
+          expandButton={({onExpand, numberOfHiddenItems}) => (
             <ExpandButtonWrapper>
             <ExpandButtonWrapper>
               <Button priority="primary" size="xsmall" onClick={onExpand}>
               <Button priority="primary" size="xsmall" onClick={onExpand}>
-                {tct('Show [numberOfCollapsedItems] More', {
-                  numberOfCollapsedItems,
-                })}
+                {tct('Show [numberOfHiddenItems] More', {numberOfHiddenItems})}
               </Button>
               </Button>
             </ExpandButtonWrapper>
             </ExpandButtonWrapper>
           )}
           )}

+ 2 - 4
tests/js/spec/components/collapsible.spec.jsx

@@ -14,9 +14,7 @@ describe('Collapsible', function () {
     expect(wrapper.find('div').length).toBe(5);
     expect(wrapper.find('div').length).toBe(5);
     expect(wrapper.find('div').at(2).text()).toBe('Item 3');
     expect(wrapper.find('div').at(2).text()).toBe('Item 3');
 
 
-    expect(wrapper.find('button[aria-label="Show 2 collapsed items"]').text()).toBe(
-      'Show 2 collapsed items'
-    );
+    expect(wrapper.find('button[aria-label="Show 2 hidden items"]').exists()).toBe(true);
     expect(wrapper.find('button[aria-label="Collapse"]').exists()).toBeFalsy();
     expect(wrapper.find('button[aria-label="Collapse"]').exists()).toBeFalsy();
   });
   });
 
 
@@ -24,7 +22,7 @@ describe('Collapsible', function () {
     const wrapper = mountWithTheme(<Collapsible>{items}</Collapsible>);
     const wrapper = mountWithTheme(<Collapsible>{items}</Collapsible>);
 
 
     // expand
     // expand
-    wrapper.find('button[aria-label="Show 2 collapsed items"]').simulate('click');
+    wrapper.find('button[aria-label="Show 2 hidden items"]').simulate('click');
 
 
     expect(wrapper.find('div').length).toBe(7);
     expect(wrapper.find('div').length).toBe(7);