Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(APP-3614): Support dropdown-item actions on ProposalActions, update DataListItem to support standalone usage #294

Merged
merged 10 commits into from
Sep 20, 2024
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Add `IconType.BLOCKCHAIN_WALLETCONNECT` and associated asset
- Add `EmptyState` fallback to ProposalActions when no actions provided
- Support `dropdownItems` property on `ProposalActions` module component to display custom actions

### Changed

- Update minor and patch versions of NPM dependencies
- Bump `micromatch` from 4.0.7 to 4.0.8
- Bump `webpack` from 5.91.0 to 5.94.0
- Update layout of `EmptyState` for centering
- Update `AccordionItem` border classes for usage within bordered containers
- Update `ProposalAction` to handle an `EmptyState` fallback for no actions passed, improve layout of children with
"Expand all" (eg. 'Execute actions' button, etc)
- Update `DataListItem` component to support button rendering and standalone usage
- Update minor and patch versions of NPM dependencies
- Bump `micromatch` from 4.0.7 to 4.0.8
- Bump `webpack` from 5.91.0 to 5.94.0
- Bump `actions/setup-python` from 5.1.1 to 5.2.0
- Bump `express` from 4.19.2 to 4.21.0

### Fixed

- Fix the `TextAreaRichText` core component to expose empty string as default value instead of empty paragraph
- Update `DropdownItem` style to correctly render items with icons aligned on the left

## [1.0.45] - 2024-08-23

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface IDataListContext
handleLoadMore: (newPage: number) => void;
}

const dataListContext = createContext<IDataListContext | null>(null);
export const dataListContext = createContext<IDataListContext | null>(null);

export const DataListContextProvider = dataListContext.Provider;

Expand Down
2 changes: 1 addition & 1 deletion src/core/components/dataList/dataListContext/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { DataListContextProvider, useDataListContext, type IDataListContext } from './dataListContext';
export { DataListContextProvider, dataListContext, useDataListContext, type IDataListContext } from './dataListContext';
32 changes: 24 additions & 8 deletions src/core/components/dataList/dataListItem/dataListItem.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,30 @@ type Story = StoryObj<typeof DataList.Item>;
* Default usage example of the DataList.Item component.
*/
export const Default: Story = {
args: {},
render: (props) => (
<DataList.Root entityLabel="Users">
<DataList.Container>
<DataList.Item {...props}>Data List Item</DataList.Item>
</DataList.Container>
</DataList.Root>
),
args: {
children: 'Data list item',
},
};

/**
* Usage of the DataList.Item component as link.
*/
export const Link: Story = {
args: {
children: 'Link data list item',
href: 'https://aragon.org',
target: '_blank',
},
};

/**
* Usage of the DataList.Item component as button.
*/
export const Button: Story = {
args: {
children: 'Button data list item',
onClick: () => null,
},
};
thekidnamedkd marked this conversation as resolved.
Show resolved Hide resolved

export default meta;
31 changes: 28 additions & 3 deletions src/core/components/dataList/dataListItem/dataListItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,29 @@ import { DataListItem, type IDataListItemProps } from './dataListItem';
describe('<DataList.Item /> component', () => {
const createTestComponent = (values?: {
props?: Partial<IDataListItemProps>;
context?: Partial<IDataListContext>;
context?: Partial<IDataListContext> | null;
}) => {
const completeProps = {
...values?.props,
};

if (values?.context === null) {
return <DataListItem {...completeProps} />;
}

return (
<DataListContextProvider value={dataListTestUtils.generateContextValues(values?.context)}>
<DataListItem {...completeProps} />
</DataListContextProvider>
);
};

it('renders a link with the given content', () => {
it('renders an interactive link with the given content when href property is set', () => {
const props = { children: 'test-data-list-item', href: '/test' };
render(createTestComponent({ props }));
expect(screen.getByRole('link', { name: props.children })).toBeInTheDocument();
const link = screen.getByRole('link', { name: props.children });
expect(link).toBeInTheDocument();
expect(link.classList).toContain('cursor-pointer');
});

it('marks the item as hidden when the data list is on initialLoading state', () => {
Expand All @@ -38,4 +44,23 @@ describe('<DataList.Item /> component', () => {
render(createTestComponent({ context, props }));
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});

it('does not throw error when not placed inside the DataListContextProvider', () => {
const context = null;
expect(() => render(createTestComponent({ context }))).not.toThrow();
});

it('renders the item as an interactive button when onClick property is set', () => {
const props = { onClick: jest.fn() };
render(createTestComponent({ props }));
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button.classList).toContain('cursor-pointer');
});

it('renders the item as a non interactive button when both onClick and href property are not set', () => {
const props = { onClick: undefined, href: undefined };
render(createTestComponent({ props }));
expect(screen.getByRole('button').classList).not.toContain('cursor-pointer');
});
});
47 changes: 27 additions & 20 deletions src/core/components/dataList/dataListItem/dataListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
import classNames from 'classnames';
import type { ComponentPropsWithoutRef } from 'react';
import { type AnchorHTMLAttributes, type ButtonHTMLAttributes, useContext } from 'react';
import { LinkBase } from '../../link';
import { useDataListContext } from '../dataListContext';
import { dataListContext } from '../dataListContext';

export interface IDataListItemProps extends ComponentPropsWithoutRef<'a'> {}
export type IDataListItemProps = ButtonHTMLAttributes<HTMLButtonElement> | AnchorHTMLAttributes<HTMLAnchorElement>;

export const DataListItem: React.FC<IDataListItemProps> = (props) => {
const { children, className, href, ...otherProps } = props;
const { className, ...otherProps } = props;

const { state, childrenItemCount } = useDataListContext();
// Use the dataListContext directly to support usage of DataListItem component outside the DataListContextProvider.
const { state, childrenItemCount } = useContext(dataListContext) ?? {};

// The DataListElement is a skeleton element on initial loading or loading state when no items are being
// rendered (e.g. after a reset filters action)
const isSkeletonElement = state === 'initialLoading' || (state === 'loading' && childrenItemCount === 0);

const isLinkElement = 'href' in otherProps && otherProps.href != null && otherProps.href !== '';
const isInteractiveElement = !isSkeletonElement && (isLinkElement || props.onClick != null);

const actionItemClasses = classNames(
'rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 transition-all', // Default
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus state
{ 'hover:border-neutral-200 hover:shadow-neutral-md active:border-neutral-300': !isSkeletonElement }, // Hover states when not skeleton
{ 'cursor-pointer': !isSkeletonElement }, // Not skeleton element
'w-full rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 outline-none transition-all focus:outline-none', // Default
{ 'focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset': isInteractiveElement }, // Interactive focus state
{ 'hover:border-neutral-200 hover:shadow-neutral-md active:border-neutral-300': isInteractiveElement }, // Interactive hover state
{ 'cursor-pointer': isInteractiveElement }, // Interactive default state
{ 'cursor-default text-left ': !isInteractiveElement }, // Non-interactive default state
'md:px-6 md:py-3.5', // Responsive
className,
);

return (
<LinkBase
className={actionItemClasses}
href={href}
aria-hidden={isSkeletonElement}
tabIndex={isSkeletonElement ? -1 : 0}
{...otherProps}
>
{children}
</LinkBase>
);
const commonProps = {
className: actionItemClasses,
'aria-hidden': isSkeletonElement,
tabIndex: isSkeletonElement ? -1 : 0,
};

if (!isLinkElement) {
const { type = 'button', ...buttonProps } = otherProps as ButtonHTMLAttributes<HTMLButtonElement>;

return <button type={type} {...commonProps} {...buttonProps} />;
}

return <LinkBase {...commonProps} {...otherProps} />;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Button } from '../../button';
import { useDataListContext } from '../dataListContext';
import { DataListItem } from '../dataListItem';
import { DataListRoot, type IDataListRootProps } from './dataListRoot';
Expand Down Expand Up @@ -29,11 +28,7 @@ describe('<DataList.Root /> component', () => {
const ChildrenComponent = () => {
const { currentPage, handleLoadMore } = useDataListContext();

return (
<DataListItem>
<Button onClick={() => handleLoadMore(currentPage + 1)}>{currentPage}</Button>
</DataListItem>
);
return <button onClick={() => handleLoadMore(currentPage + 1)}>{currentPage}</button>;
};

render(
Expand Down
6 changes: 3 additions & 3 deletions src/core/components/dropdown/dropdownItem/dropdownItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ export const DropdownItem: React.FC<IDropdownItemProps> = (props) => {
disabled={disabled}
asChild={renderLink}
className={classNames(
'flex items-center justify-between gap-3 px-4 py-3', // Layout
'flex items-center gap-3 px-4 py-3', // Layout
'cursor-pointer rounded-xl text-base leading-tight focus-visible:outline-none', // Style
'data-[disabled]:cursor-default data-[disabled]:bg-neutral-0 data-[disabled]:text-neutral-300', // Disabled
{ 'bg-neutral-0 text-neutral-500': !selected && !disabled }, // Not selected
{ 'bg-neutral-50 text-neutral-800': selected && !disabled }, // Selected
{ 'hover:bg-neutral-50 hover:text-neutral-800': !disabled }, // Hover
{ 'data-[highlighted]:bg-neutral-50 data-[highlighted]:text-neutral-800': !disabled }, // Highlighted
{ 'flex-row': iconPosition === 'right' },
{ 'flex-row-reverse': iconPosition === 'left' && icon != null },
{ 'flex-row justify-between': iconPosition === 'right' },
{ 'flex-row-reverse justify-end': iconPosition === 'left' && icon != null },
className,
)}
{...otherProps}
Expand Down
1 change: 1 addition & 0 deletions src/modules/assets/copy/modulesCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const modulesCopy = {
},
},
proposalActionsAction: {
dropdownLabel: 'More',
notVerified: 'Not verified',
nativeSendAlert: 'Proceed with caution',
nativeSendDescription: (amount: string) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DataList } from '../../../../../core';
import { AssetDataListItem } from '../../assetDataListItem';

const meta: Meta<typeof AssetDataListItem.Skeleton> = {
Expand All @@ -18,13 +17,6 @@ type Story = StoryObj<typeof AssetDataListItem.Skeleton>;
/**
* Default usage example of the DaoDataListItemSkeleton component.
*/
export const Default: Story = {
args: {},
render: () => (
<DataList.Root entityLabel="Asset" state="initialLoading" pageSize={1}>
<DataList.Container SkeletonElement={AssetDataListItem.Skeleton} />
</DataList.Root>
),
};
export const Default: Story = {};

export default meta;
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { render, screen } from '@testing-library/react';
import { DataList } from '../../../../../core';
import { AssetDataListItem, type IAssetDataListItemSkeletonProps } from '../../assetDataListItem';

describe('<AssetDataListItem.Skeleton /> component', () => {
const createTestComponent = (props?: Partial<IAssetDataListItemSkeletonProps>) => {
const completeProps: IAssetDataListItemSkeletonProps = { ...props };

return (
<DataList.Root entityLabel="Asset">
<AssetDataListItem.Skeleton {...completeProps} />
</DataList.Root>
);
return <AssetDataListItem.Skeleton {...completeProps} />;
};
it('has correct accessibility attributes', () => {
render(createTestComponent());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { DataList, type IDataListItemProps } from '../../../../../core';
import { StateSkeletonBar } from '../../../../../core/components/states/stateSkeletonBar';
import { StateSkeletonCircular } from '../../../../../core/components/states/stateSkeletonCircular';

export interface IAssetDataListItemSkeletonProps extends IDataListItemProps {}
export type IAssetDataListItemSkeletonProps = IDataListItemProps;

export const AssetDataListItemSkeleton: React.FC<IAssetDataListItemSkeletonProps> = (props) => {
const { className, ...otherProps } = props;

return (
<DataList.Item
tabIndex={0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DataList } from '../../../../../core';
import { AssetDataListItemStructure } from './assetDataListItemStructure';

const meta: Meta<typeof AssetDataListItemStructure> = {
Expand Down Expand Up @@ -27,13 +26,6 @@ export const Default: Story = {
fiatPrice: 3654.76,
priceChange: 15,
},
render: (props) => (
<DataList.Root entityLabel="Assets">
<DataList.Container>
<AssetDataListItemStructure {...props} />
</DataList.Container>
</DataList.Root>
),
};

/**
Expand All @@ -48,13 +40,6 @@ export const LongName: Story = {
fiatPrice: 3654.76,
priceChange: 15,
},
render: (props) => (
<DataList.Root entityLabel="Assets">
<DataList.Container>
<AssetDataListItemStructure {...props} />
</DataList.Container>
</DataList.Root>
),
};

/**
Expand All @@ -66,13 +51,6 @@ export const Fallback: Story = {
amount: 420.69,
symbol: 'ETH',
},
render: (props) => (
<DataList.Root entityLabel="Assets">
<DataList.Container>
<AssetDataListItemStructure {...props} />
</DataList.Container>
</DataList.Root>
),
};

export default meta;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { render, screen } from '@testing-library/react';
import { DataList } from '../../../../../core';
import { AssetDataListItemStructure, type IAssetDataListItemStructureProps } from './assetDataListItemStructure';

describe('<AssetDataListItem.Structure /> component', () => {
Expand All @@ -11,13 +10,7 @@ describe('<AssetDataListItem.Structure /> component', () => {
...props,
};

return (
<DataList.Root entityLabel="Assets">
<DataList.Container>
<AssetDataListItemStructure {...completeProps} />
</DataList.Container>
</DataList.Root>
);
return <AssetDataListItemStructure {...completeProps} />;
};

it('renders token name and symbol', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { Avatar, DataList, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
import { useOdsModulesContext } from '../../../odsModulesProvider';

export interface IAssetDataListItemStructureProps extends IDataListItemProps {
export type IAssetDataListItemStructureProps = IDataListItemProps & {
/**
* The name of the asset.
*/
Expand All @@ -30,7 +30,7 @@ export interface IAssetDataListItemStructureProps extends IDataListItemProps {
* @default 0
*/
priceChange?: number;
}
};

export const AssetDataListItemStructure: React.FC<IAssetDataListItemStructureProps> = (props) => {
const { logoSrc, name, amount, symbol, fiatPrice, priceChange = 0, ...otherProps } = props;
Expand Down
Loading