From a808f83e7dba611e3530738825999cf23e4541e7 Mon Sep 17 00:00:00 2001
From: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date: Wed, 15 May 2024 08:26:23 +0300
Subject: [PATCH] fix(ui): use expand button in sidebar and tables [EE-6844]
 (#11608)

---
 .../components/CollapseExpandButton.test.tsx  | 68 +++++++++++++++++++
 app/react/components/CollapseExpandButton.tsx | 41 +++++++++++
 .../components/datatables/expand-column.tsx   | 30 +++-----
 .../FormSection/FormSection.tsx               | 27 +++-----
 .../sidebar/SidebarItem/SidebarParent.tsx     | 50 +++++++-------
 5 files changed, 155 insertions(+), 61 deletions(-)
 create mode 100644 app/react/components/CollapseExpandButton.test.tsx
 create mode 100644 app/react/components/CollapseExpandButton.tsx

diff --git a/app/react/components/CollapseExpandButton.test.tsx b/app/react/components/CollapseExpandButton.test.tsx
new file mode 100644
index 000000000..6ea14603d
--- /dev/null
+++ b/app/react/components/CollapseExpandButton.test.tsx
@@ -0,0 +1,68 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { CollapseExpandButton } from './CollapseExpandButton';
+
+it('should render the button with the correct icon and title', () => {
+  renderCollapseExpandButton();
+  const button = screen.getByRole('button');
+
+  expect(button).toBeInTheDocument();
+  expect(button).toHaveAttribute('title', 'Expand');
+  expect(button).toHaveAttribute('aria-label', 'Expand');
+  expect(button).toHaveAttribute('aria-expanded', 'false');
+  expect(button.querySelector('svg')).toBeInTheDocument();
+});
+
+it('should call the onClick handler when the button is clicked', async () => {
+  const onClick = vi.fn();
+  const { user } = renderCollapseExpandButton({ onClick });
+  const button = screen.getByRole('button');
+
+  await user.click(button);
+
+  expect(onClick).toHaveBeenCalledTimes(1);
+});
+
+it('should prevent default and stop propagation when the button is clicked', async () => {
+  const user = userEvent.setup();
+  const onClick = vi.fn();
+  const onOuterClick = vi.fn();
+
+  render(
+    // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+    <div onClick={onOuterClick}>
+      <CollapseExpandButton
+        onClick={onClick}
+        isExpanded={false}
+        data-cy="nothing"
+      />
+    </div>
+  );
+
+  const button = screen.getByLabelText('Expand');
+
+  await user.click(button);
+
+  expect(onOuterClick).not.toHaveBeenCalled();
+  expect(onClick).toHaveBeenCalled();
+});
+
+function renderCollapseExpandButton({
+  isExpanded = false,
+  onClick = vi.fn(),
+}: {
+  isExpanded?: boolean;
+  onClick?(): void;
+} = {}) {
+  const user = userEvent.setup();
+
+  render(
+    <CollapseExpandButton
+      isExpanded={isExpanded}
+      data-cy="random"
+      onClick={onClick}
+    />
+  );
+  return { user };
+}
diff --git a/app/react/components/CollapseExpandButton.tsx b/app/react/components/CollapseExpandButton.tsx
new file mode 100644
index 000000000..3fff465d9
--- /dev/null
+++ b/app/react/components/CollapseExpandButton.tsx
@@ -0,0 +1,41 @@
+import { ChevronDown } from 'lucide-react';
+import { ComponentProps } from 'react';
+import clsx from 'clsx';
+
+import { Icon } from './Icon';
+
+export function CollapseExpandButton({
+  onClick,
+  isExpanded,
+  ...props
+}: { isExpanded: boolean } & ComponentProps<'button'>) {
+  return (
+    <button
+      onClick={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+
+        onClick?.(e);
+      }}
+      color="none"
+      title={isExpanded ? 'Collapse' : 'Expand'}
+      aria-label={isExpanded ? 'Collapse' : 'Expand'}
+      aria-expanded={isExpanded}
+      type="button"
+      className="flex-none border-none bg-transparent flex items-center p-0 px-3 group"
+      // eslint-disable-next-line react/jsx-props-no-spreading
+      {...props}
+    >
+      <div className="flex items-center group-hover:bg-blue-5 be:group-hover:bg-gray-5 group-hover:th-dark:bg-gray-true-7 group-hover:bg-opacity-10 be:group-hover:bg-opacity-10 rounded-full p-[3px] transition ease-in-out">
+        <Icon
+          icon={ChevronDown}
+          size="md"
+          className={clsx('transition ease-in-out', {
+            'rotate-180': isExpanded,
+            'rotate-0': !isExpanded,
+          })}
+        />
+      </div>
+    </button>
+  );
+}
diff --git a/app/react/components/datatables/expand-column.tsx b/app/react/components/datatables/expand-column.tsx
index a4678760e..3f09aaf58 100644
--- a/app/react/components/datatables/expand-column.tsx
+++ b/app/react/components/datatables/expand-column.tsx
@@ -1,7 +1,6 @@
-import { ChevronDown, ChevronUp } from 'lucide-react';
 import { ColumnDef } from '@tanstack/react-table';
 
-import { Button } from '@@/buttons';
+import { CollapseExpandButton } from '../CollapseExpandButton';
 
 import { DefaultType } from './types';
 
@@ -13,32 +12,25 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
 
       return (
         hasExpandableItems && (
-          <Button
+          <CollapseExpandButton
+            isExpanded={table.getIsAllRowsExpanded()}
             onClick={table.getToggleAllRowsExpandedHandler()}
-            color="none"
-            icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
-            title="Expand all"
             data-cy="expand-all-rows-button"
-            aria-label="Expand all rows"
+            aria-label={
+              table.getIsAllRowsExpanded()
+                ? 'Collapse all rows'
+                : 'Expand all rows'
+            }
           />
         )
       );
     },
     cell: ({ row }) =>
       row.getCanExpand() && (
-        <Button
-          onClick={(e) => {
-            e.preventDefault();
-            e.stopPropagation();
-
-            row.toggleExpanded();
-          }}
-          color="none"
-          icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
-          title={row.getIsExpanded() ? 'Collapse' : 'Expand'}
+        <CollapseExpandButton
+          isExpanded={row.getIsExpanded()}
+          onClick={row.getToggleExpandedHandler()}
           data-cy={`expand-row-button_${row.index}`}
-          aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
-          aria-expanded={row.getIsExpanded()}
         />
       ),
     enableColumnFilter: false,
diff --git a/app/react/components/form-components/FormSection/FormSection.tsx b/app/react/components/form-components/FormSection/FormSection.tsx
index 20725a95f..05bb9ddbf 100644
--- a/app/react/components/form-components/FormSection/FormSection.tsx
+++ b/app/react/components/form-components/FormSection/FormSection.tsx
@@ -1,7 +1,6 @@
 import { PropsWithChildren, ReactNode, useState } from 'react';
-import { ChevronUp, ChevronRight } from 'lucide-react';
 
-import { Icon } from '@@/Icon';
+import { CollapseExpandButton } from '@@/CollapseExpandButton';
 
 import { FormSectionTitle } from '../FormSectionTitle';
 
@@ -26,30 +25,22 @@ export function FormSection({
   htmlFor = '',
 }: PropsWithChildren<Props>) {
   const [isExpanded, setIsExpanded] = useState(!defaultFolded);
+  const id = `foldingButton${title}`;
 
   return (
     <div className={className}>
       <FormSectionTitle
-        htmlFor={isFoldable ? `foldingButton${title}` : htmlFor}
+        htmlFor={isFoldable ? id : htmlFor}
         titleSize={titleSize}
         className={titleClassName}
       >
         {isFoldable && (
-          <button
-            id={`foldingButton${title}`}
-            type="button"
-            onClick={(e) => {
-              setIsExpanded(!isExpanded);
-              e.stopPropagation();
-              e.preventDefault();
-            }}
-            className="mx-2 !ml-0 inline-flex w-2 items-center justify-center border-0 bg-transparent"
-          >
-            <Icon
-              icon={isExpanded ? ChevronUp : ChevronRight}
-              className="shrink-0"
-            />
-          </button>
+          <CollapseExpandButton
+            isExpanded={isExpanded}
+            data-cy={id}
+            id={id}
+            onClick={() => setIsExpanded((isExpanded) => !isExpanded)}
+          />
         )}
 
         {title}
diff --git a/app/react/sidebar/SidebarItem/SidebarParent.tsx b/app/react/sidebar/SidebarItem/SidebarParent.tsx
index ed6048822..097472fc8 100644
--- a/app/react/sidebar/SidebarItem/SidebarParent.tsx
+++ b/app/react/sidebar/SidebarItem/SidebarParent.tsx
@@ -1,11 +1,11 @@
 import clsx from 'clsx';
-import { ChevronDown } from 'lucide-react';
 import { PropsWithChildren, useState } from 'react';
 
 import { AutomationTestingProps } from '@/types';
 
 import { Icon } from '@@/Icon';
 import { Link } from '@@/Link';
+import { CollapseExpandButton } from '@@/CollapseExpandButton';
 
 import { useSidebarState } from '../useSidebarState';
 
@@ -79,29 +79,11 @@ export function SidebarParent({
           </Link>
         </button>
         {isSidebarOpen && (
-          <button
-            type="button"
-            className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
-            onClick={(e) => {
-              e.preventDefault();
-              e.stopPropagation();
-              setIsExpanded((isExpanded) => !isExpanded);
-            }}
-            title={isExpanded ? 'Collapse' : 'Expand'}
-            aria-expanded={isExpanded}
-            aria-controls={listId}
-          >
-            <div className="flex items-center group-hover:bg-blue-5 be:group-hover:bg-gray-5 group-hover:th-dark:bg-gray-true-7 group-hover:bg-opacity-10 be:group-hover:bg-opacity-10 rounded-full p-[3px] transition ease-in-out">
-              <Icon
-                icon={ChevronDown}
-                size="md"
-                className={clsx('transition ease-in-out', {
-                  'rotate-180': isExpanded,
-                  'rotate-0': !isExpanded,
-                })}
-              />
-            </div>
-          </button>
+          <SidebarExpandButton
+            onClick={() => setIsExpanded((isExpanded) => !isExpanded)}
+            isExpanded={isExpanded}
+            listId={listId}
+          />
         )}
       </div>
     </Wrapper>
@@ -145,3 +127,23 @@ export function SidebarParent({
     </SidebarTooltip>
   );
 }
+
+function SidebarExpandButton({
+  isExpanded,
+  listId,
+  onClick,
+}: {
+  onClick(): void;
+  isExpanded: boolean;
+  listId: string;
+}) {
+  return (
+    <CollapseExpandButton
+      isExpanded={isExpanded}
+      onClick={onClick}
+      aria-controls={listId}
+      data-cy="expand-button"
+      className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
+    />
+  );
+}