Skip to content

Commit

Permalink
feat(APP-3668): Update ProposalVotingTabs component to disable Breakd…
Browse files Browse the repository at this point in the history
…own / Votes tabs when status is not active (#306)
  • Loading branch information
cgero-eth authored Oct 4, 2024
1 parent 61df7bc commit e72b775
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 101 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Update `Tabs` core component to handle disabled tab trigger state
- Support `forceMount` property on `Accordion` core component and `ProposalVotingStage` module component to correctly
render dynamic content on proposal stages.

### Fixed

- Fix truncation issue on `VoteProposalDataListItem` module component
- Update `AddressInput` module component to forward `chainId` and `wagmiConfig` to `MemberAvatar` component

### Changed

- Update `<ProposalVotingTabs />` module component to disable `Breakdown` and `Votes` tabs when voting status is not
active
- Bump `actions/setup-node` from 4.0.3 to 4.0.4
- Bump `actions/checkout` from 4.1.7 to 4.2.0
- Update minor and patch dependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { type RefAttributes } from 'react';
import { Accordion, type IAccordionContainerProps } from '..';
import { Accordion } from '..';

/**
* Accordion.Container can contain multiple Accordion.Items which comprises an Accordion.Header and its collapsible Accordion.Content.
*/
const meta: Meta<typeof Accordion.Container> = {
title: 'Core/Components/Accordion/Accordion.Container',
component: Accordion.Container,
Expand All @@ -18,44 +14,57 @@ const meta: Meta<typeof Accordion.Container> = {

type Story = StoryObj<typeof Accordion.Container>;

const reusableStoryComponent = (props: IAccordionContainerProps & RefAttributes<HTMLDivElement>, count: number) => {
return (
<Accordion.Container {...props}>
{Array.from({ length: count }, (_, index) => (
<Accordion.Item key={`item-${index + 1}`} value={`item-${index + 1}`}>
<Accordion.ItemHeader>Item {index + 1} Header</Accordion.ItemHeader>
<Accordion.ItemContent>
<div className="flex h-24 w-full items-center justify-center border border-dashed border-info-300 bg-info-100">
Item {index + 1} Content
</div>
</Accordion.ItemContent>
</Accordion.Item>
))}
</Accordion.Container>
);
};
const DefaultChildComponent = (childCount: number, forceMount?: true) =>
[...Array(childCount)].map((_, index) => (
<Accordion.Item key={`item-${index}`} value={`item-${index}`}>
<Accordion.ItemHeader>Item {index + 1} Header</Accordion.ItemHeader>
<Accordion.ItemContent forceMount={forceMount}>
<div className="flex h-24 w-full items-center justify-center border border-dashed border-info-300 bg-info-100">
Item {index + 1} Content
</div>
</Accordion.ItemContent>
</Accordion.Item>
));

/**
* Default usage example of a full Accordion component.
* Default usage example of the Accordion component.
*/
export const Default: Story = {
args: { isMulti: false },
render: (args) => reusableStoryComponent(args as IAccordionContainerProps & RefAttributes<HTMLDivElement>, 1),
args: {
isMulti: false,
children: DefaultChildComponent(2),
},
};

/**
* Example of an Accordion component with multiple items open at the same time.
*/
export const MultiType: Story = {
args: {
isMulti: true,
children: DefaultChildComponent(3),
},
};

/**
* Example of an Accordion component implementation with a type of "single" and no defaultValue is set.
* Example of an Accordion component with two accordion item open by default.
*/
export const SingleTypeItems: Story = {
args: { isMulti: false },
render: (args) => reusableStoryComponent(args as IAccordionContainerProps & RefAttributes<HTMLDivElement>, 3),
export const DefaultValue: Story = {
args: {
isMulti: true,
children: DefaultChildComponent(3),
defaultValue: ['item-1', 'item-2'],
},
};

/**
* Example of an Accordion component implementation with a type of "multiple" where the second and third items have been set as the defaultValue.
* Use the `forceMount` property to always render the accordion item content.
*/
export const MultipleTypeItems: Story = {
args: { isMulti: true, defaultValue: ['item-2', 'item-3'] },
render: (args) => reusableStoryComponent(args as IAccordionContainerProps & RefAttributes<HTMLDivElement>, 3),
export const ForceMount: Story = {
args: {
isMulti: true,
children: DefaultChildComponent(3, true),
},
};

export default meta;
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ import { AccordionContent as RadixAccordionContent } from '@radix-ui/react-accor
import classNames from 'classnames';
import { forwardRef, type ComponentPropsWithRef } from 'react';

export interface IAccordionItemContentProps extends ComponentPropsWithRef<'div'> {}
export interface IAccordionItemContentProps extends ComponentPropsWithRef<'div'> {
/**
* Forces the content to be mounted when set to true.
*/
forceMount?: true;
}

export const AccordionItemContent = forwardRef<HTMLDivElement, IAccordionItemContentProps>((props, ref) => {
const { children, className, ...otherProps } = props;
const { children, className, forceMount, ...otherProps } = props;

const contentClassNames = classNames(
'overflow-hidden', // Default
{ 'data-[state=closed]:hidden': forceMount }, // Force mount variant
'data-[state=open]:animate-[accordionExpand_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // Expanding animation
'data-[state=closed]:animate-[accordionCollapse_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // Collapsing animation
className,
);

return (
<RadixAccordionContent
className={classNames(
'overflow-hidden', // default styles
'data-[state=open]:animate-[accordionExpand_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // expanding animation
'data-[state=closed]:animate-[accordionCollapse_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // collapsing animation
className,
)}
ref={ref}
{...otherProps}
>
<RadixAccordionContent forceMount={forceMount} className={contentClassNames} ref={ref} {...otherProps}>
<div className="px-4 pb-4 pt-1 md:px-6 md:pb-6">{children}</div>
</RadixAccordionContent>
);
Expand Down
8 changes: 4 additions & 4 deletions src/core/components/tabs/tabsRoot/tabsRoot.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const reusableStoryComponent = (props: ITabsRootProps) => {
return (
<Tabs.Root {...props}>
<Tabs.List>
<Tabs.Trigger label="Tab 1" value="1" />
<Tabs.Trigger label="Tab 2" value="2" />
<Tabs.Trigger label="Tab 3" value="3" iconRight={IconType.BLOCKCHAIN_BLOCK} />
<Tabs.Trigger label="Default Tab" value="1" />
<Tabs.Trigger label="Disabled Tab" value="2" disabled={true} />
<Tabs.Trigger label="Icon Tab" value="3" iconRight={IconType.BLOCKCHAIN_BLOCK} />
</Tabs.List>
<Tabs.Content value="1">
<div className="flex h-24 w-96 items-center justify-center border border-dashed border-info-300 bg-info-100">
Expand Down Expand Up @@ -66,7 +66,7 @@ export const Underlined: Story = {
* Usage example of a Tabs component inside a Card component with the defaultValue set.
*/
export const InsideCard: Story = {
args: { defaultValue: '2' },
args: { defaultValue: '3' },
render: (args) => <Card className="p-6">{reusableStoryComponent(args)}</Card>,
};

Expand Down
41 changes: 25 additions & 16 deletions src/core/components/tabs/tabsTrigger/tabsTrigger.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Tabs, type ITabsTriggerProps } from '..';
import type { ComponentType } from 'react';
import { Tabs } from '..';
import { IconType } from '../../icon';

const ComponentWrapper = (Story: ComponentType) => (
<Tabs.Root>
<Tabs.List>
<Story />
</Tabs.List>
</Tabs.Root>
);

/**
* Tabs.Root can contain multiple Tabs.Triggers inside it's requisite Tabs.List. These tabs will coordinate with what Tabs.Content to show by matching their value prop.
*/
const meta: Meta<typeof Tabs.Trigger> = {
title: 'Core/Components/Tabs/Tabs.Trigger',
component: Tabs.Trigger,
decorators: ComponentWrapper,
parameters: {
design: {
type: 'figma',
Expand All @@ -17,25 +25,26 @@ const meta: Meta<typeof Tabs.Trigger> = {

type Story = StoryObj<typeof Tabs.Trigger>;

const reusableStoryComponent = (props: ITabsTriggerProps) => {
return (
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger {...props} />
</Tabs.List>
</Tabs.Root>
);
/**
* Default usage example of a single Tabs.Trigger component.
*/
export const Default: Story = {
args: {
label: 'Example',
value: 'example',
},
};

/**
* Default usage example of a single Tabs.Trigger component.
*/
export const Default: Story = {
export const Disabled: Story = {
args: {
label: 'Tab 1',
value: '1',
label: 'Disabled tab',
value: 'disabled',
iconRight: IconType.APP_ASSETS,
disabled: true,
},
render: (args) => reusableStoryComponent(args),
};

export default meta;
28 changes: 13 additions & 15 deletions src/core/components/tabs/tabsTrigger/tabsTrigger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,38 @@ import { IconType } from '../../icon';
import { Tabs, type ITabsTriggerProps } from '../../tabs';

describe('<Tabs.Trigger /> component', () => {
const createTestComponent = (props?: Partial<ITabsTriggerProps>, isUnderlined?: boolean) => {
const createTestComponent = (props?: Partial<ITabsTriggerProps>) => {
const completeProps: ITabsTriggerProps = {
label: 'Tab 1',
value: '1',
...props,
};

return (
<Tabs.Root isUnderlined={isUnderlined}>
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger {...completeProps} />
</Tabs.List>
</Tabs.Root>
);
};

it('should render without crashing', () => {
it('renders a tab', () => {
render(createTestComponent());

expect(screen.getByRole('tab')).toBeInTheDocument();
const tab = screen.getByRole('tab');
expect(tab).toBeInTheDocument();
expect(tab.getAttribute('disabled')).toBeNull();
});

it('should pass the correct value prop', () => {
const value = 'complex1';
render(createTestComponent({ value }));

const triggerElement = screen.getByRole('tab');
expect(triggerElement).toHaveAttribute('id', `radix-:r2:-trigger-${value}`);
});

it('should render the icon when iconRight is provided', () => {
it('renders the icon when iconRight is provided', () => {
const iconRight = IconType.BLOCKCHAIN_BLOCK;
render(createTestComponent({ iconRight }));
expect(screen.getByTestId(iconRight)).toBeInTheDocument();
});

expect(screen.getByTestId('BLOCKCHAIN_BLOCK')).toBeInTheDocument();
it('disables the tab when the disabled property is set to true', () => {
const disabled = true;
render(createTestComponent({ disabled }));
expect(screen.getByRole('tab').getAttribute('disabled')).toEqual('');
});
});
26 changes: 13 additions & 13 deletions src/core/components/tabs/tabsTrigger/tabsTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,29 @@ export interface ITabsTriggerProps extends ComponentProps<'button'> {
}

export const TabsTrigger: React.FC<ITabsTriggerProps> = (props) => {
const { label, iconRight, className, value, ...otherProps } = props;
const { label, iconRight, className, value, disabled, ...otherProps } = props;
const { isUnderlined } = useContext(TabsContext);

const triggerClassNames = classNames(
'group line-clamp-1 flex cursor-pointer items-center gap-x-4 rounded-t border-primary-400 py-3 text-base font-normal leading-tight text-neutral-500', // base
'hover:text-neutral-800', // hover
'active:data-[state=active]:text-neutral-800 active:data-[state=active]:shadow-[inset_0_0_0_0,0_1px_0_0] active:data-[state=active]:shadow-primary-400', // active click
'focus:outline-none', // focus -- might need style updates pending conversation
{ 'hover:shadow-[inset_0_0_0_0,0_1px_0_0] hover:shadow-neutral-800': isUnderlined }, // isUnderlined variant
'data-[state=active]:text-neutral-800 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-primary-400', // active selection
'group line-clamp-1 flex items-center gap-x-4 rounded-t border-primary-400 py-3 text-base font-normal leading-tight', // Base
'active:data-[state=active]:text-neutral-800 active:data-[state=active]:shadow-[inset_0_0_0_0,0_1px_0_0] active:data-[state=active]:shadow-primary-400', // Active state
'focus:outline-none', // Focus state
{ 'hover:shadow-[inset_0_0_0_0,0_1px_0_0] hover:shadow-neutral-800': isUnderlined && !disabled }, // Underlined & enabled variant
'data-[state=active]:text-neutral-800 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-primary-400', // Active selection
{ 'cursor-pointer text-neutral-500 hover:text-neutral-800': !disabled }, // Enabled state
{ 'text-neutral-300': disabled }, // Disabled state
className,
);

const iconClassNames = classNames(
'group-data-[state=active]:text-neutral-800',
'text-neutral-500',
'group-hover:text-neutral-300',
'group-active:text-neutral-600',
'group-focus:text-neutral-500',
'group-data-[state=active]:text-neutral-800', // Base
{ 'text-neutral-200': disabled }, // Disabled state
{ 'text-neutral-500 group-hover:text-neutral-300': !disabled }, // Enabled state
{ 'group-focus:text-neutral-500 group-active:text-neutral-600': !disabled }, // Enabled & Active/Focus states
);

return (
<RadixTabsTrigger className={triggerClassNames} value={value} {...otherProps}>
<RadixTabsTrigger className={triggerClassNames} value={value} disabled={disabled} {...otherProps}>
{label}
{iconRight && <Icon icon={iconRight} size="sm" className={iconClassNames} />}
</RadixTabsTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { IProposalActionsActionRawViewProps, ProposalActionsActionRawView } from './proposalActionsActionRawView';
export { ProposalActionsActionRawView, type IProposalActionsActionRawViewProps } from './proposalActionsActionRawView';
Loading

0 comments on commit e72b775

Please sign in to comment.