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" + /> + ); +}