mirror of https://github.com/portainer/portainer
fix(ui): use expand button in sidebar and tables [EE-6844] (#11608)
parent
413b9c3b04
commit
a808f83e7d
|
@ -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 };
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Button } from '@@/buttons';
|
import { CollapseExpandButton } from '../CollapseExpandButton';
|
||||||
|
|
||||||
import { DefaultType } from './types';
|
import { DefaultType } from './types';
|
||||||
|
|
||||||
|
@ -13,32 +12,25 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasExpandableItems && (
|
hasExpandableItems && (
|
||||||
<Button
|
<CollapseExpandButton
|
||||||
|
isExpanded={table.getIsAllRowsExpanded()}
|
||||||
onClick={table.getToggleAllRowsExpandedHandler()}
|
onClick={table.getToggleAllRowsExpandedHandler()}
|
||||||
color="none"
|
|
||||||
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
|
|
||||||
title="Expand all"
|
|
||||||
data-cy="expand-all-rows-button"
|
data-cy="expand-all-rows-button"
|
||||||
aria-label="Expand all rows"
|
aria-label={
|
||||||
|
table.getIsAllRowsExpanded()
|
||||||
|
? 'Collapse all rows'
|
||||||
|
: 'Expand all rows'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.getCanExpand() && (
|
row.getCanExpand() && (
|
||||||
<Button
|
<CollapseExpandButton
|
||||||
onClick={(e) => {
|
isExpanded={row.getIsExpanded()}
|
||||||
e.preventDefault();
|
onClick={row.getToggleExpandedHandler()}
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
row.toggleExpanded();
|
|
||||||
}}
|
|
||||||
color="none"
|
|
||||||
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
|
|
||||||
title={row.getIsExpanded() ? 'Collapse' : 'Expand'}
|
|
||||||
data-cy={`expand-row-button_${row.index}`}
|
data-cy={`expand-row-button_${row.index}`}
|
||||||
aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
|
|
||||||
aria-expanded={row.getIsExpanded()}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableColumnFilter: false,
|
enableColumnFilter: false,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { PropsWithChildren, ReactNode, useState } from 'react';
|
import { PropsWithChildren, ReactNode, useState } from 'react';
|
||||||
import { ChevronUp, ChevronRight } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { CollapseExpandButton } from '@@/CollapseExpandButton';
|
||||||
|
|
||||||
import { FormSectionTitle } from '../FormSectionTitle';
|
import { FormSectionTitle } from '../FormSectionTitle';
|
||||||
|
|
||||||
|
@ -26,30 +25,22 @@ export function FormSection({
|
||||||
htmlFor = '',
|
htmlFor = '',
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
||||||
|
const id = `foldingButton${title}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<FormSectionTitle
|
<FormSectionTitle
|
||||||
htmlFor={isFoldable ? `foldingButton${title}` : htmlFor}
|
htmlFor={isFoldable ? id : htmlFor}
|
||||||
titleSize={titleSize}
|
titleSize={titleSize}
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
{isFoldable && (
|
{isFoldable && (
|
||||||
<button
|
<CollapseExpandButton
|
||||||
id={`foldingButton${title}`}
|
isExpanded={isExpanded}
|
||||||
type="button"
|
data-cy={id}
|
||||||
onClick={(e) => {
|
id={id}
|
||||||
setIsExpanded(!isExpanded);
|
onClick={() => setIsExpanded((isExpanded) => !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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ChevronDown } from 'lucide-react';
|
|
||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
import { CollapseExpandButton } from '@@/CollapseExpandButton';
|
||||||
|
|
||||||
import { useSidebarState } from '../useSidebarState';
|
import { useSidebarState } from '../useSidebarState';
|
||||||
|
|
||||||
|
@ -79,29 +79,11 @@ export function SidebarParent({
|
||||||
</Link>
|
</Link>
|
||||||
</button>
|
</button>
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<button
|
<SidebarExpandButton
|
||||||
type="button"
|
onClick={() => setIsExpanded((isExpanded) => !isExpanded)}
|
||||||
className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
|
isExpanded={isExpanded}
|
||||||
onClick={(e) => {
|
listId={listId}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
@ -145,3 +127,23 @@ export function SidebarParent({
|
||||||
</SidebarTooltip>
|
</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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue