Skip to content

Commit e72b775

Browse files
authored
feat(APP-3668): Update ProposalVotingTabs component to disable Breakdown / Votes tabs when status is not active (#306)
1 parent 61df7bc commit e72b775

File tree

12 files changed

+177
-101
lines changed

12 files changed

+177
-101
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Update `Tabs` core component to handle disabled tab trigger state
13+
- Support `forceMount` property on `Accordion` core component and `ProposalVotingStage` module component to correctly
14+
render dynamic content on proposal stages.
15+
1016
### Fixed
1117

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

1521
### Changed
1622

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

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

1915
type Story = StoryObj<typeof Accordion.Container>;
2016

21-
const reusableStoryComponent = (props: IAccordionContainerProps & RefAttributes<HTMLDivElement>, count: number) => {
22-
return (
23-
<Accordion.Container {...props}>
24-
{Array.from({ length: count }, (_, index) => (
25-
<Accordion.Item key={`item-${index + 1}`} value={`item-${index + 1}`}>
26-
<Accordion.ItemHeader>Item {index + 1} Header</Accordion.ItemHeader>
27-
<Accordion.ItemContent>
28-
<div className="flex h-24 w-full items-center justify-center border border-dashed border-info-300 bg-info-100">
29-
Item {index + 1} Content
30-
</div>
31-
</Accordion.ItemContent>
32-
</Accordion.Item>
33-
))}
34-
</Accordion.Container>
35-
);
36-
};
17+
const DefaultChildComponent = (childCount: number, forceMount?: true) =>
18+
[...Array(childCount)].map((_, index) => (
19+
<Accordion.Item key={`item-${index}`} value={`item-${index}`}>
20+
<Accordion.ItemHeader>Item {index + 1} Header</Accordion.ItemHeader>
21+
<Accordion.ItemContent forceMount={forceMount}>
22+
<div className="flex h-24 w-full items-center justify-center border border-dashed border-info-300 bg-info-100">
23+
Item {index + 1} Content
24+
</div>
25+
</Accordion.ItemContent>
26+
</Accordion.Item>
27+
));
28+
3729
/**
38-
* Default usage example of a full Accordion component.
30+
* Default usage example of the Accordion component.
3931
*/
4032
export const Default: Story = {
41-
args: { isMulti: false },
42-
render: (args) => reusableStoryComponent(args as IAccordionContainerProps & RefAttributes<HTMLDivElement>, 1),
33+
args: {
34+
isMulti: false,
35+
children: DefaultChildComponent(2),
36+
},
37+
};
38+
39+
/**
40+
* Example of an Accordion component with multiple items open at the same time.
41+
*/
42+
export const MultiType: Story = {
43+
args: {
44+
isMulti: true,
45+
children: DefaultChildComponent(3),
46+
},
4347
};
4448

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

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

6170
export default meta;

src/core/components/accordion/accordionItemContent/accordionItemContent.tsx

+16-12
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@ import { AccordionContent as RadixAccordionContent } from '@radix-ui/react-accor
22
import classNames from 'classnames';
33
import { forwardRef, type ComponentPropsWithRef } from 'react';
44

5-
export interface IAccordionItemContentProps extends ComponentPropsWithRef<'div'> {}
5+
export interface IAccordionItemContentProps extends ComponentPropsWithRef<'div'> {
6+
/**
7+
* Forces the content to be mounted when set to true.
8+
*/
9+
forceMount?: true;
10+
}
611

712
export const AccordionItemContent = forwardRef<HTMLDivElement, IAccordionItemContentProps>((props, ref) => {
8-
const { children, className, ...otherProps } = props;
13+
const { children, className, forceMount, ...otherProps } = props;
14+
15+
const contentClassNames = classNames(
16+
'overflow-hidden', // Default
17+
{ 'data-[state=closed]:hidden': forceMount }, // Force mount variant
18+
'data-[state=open]:animate-[accordionExpand_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // Expanding animation
19+
'data-[state=closed]:animate-[accordionCollapse_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // Collapsing animation
20+
className,
21+
);
922

1023
return (
11-
<RadixAccordionContent
12-
className={classNames(
13-
'overflow-hidden', // default styles
14-
'data-[state=open]:animate-[accordionExpand_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // expanding animation
15-
'data-[state=closed]:animate-[accordionCollapse_0.3s_cubic-bezier(0.87,_0,_0.13,_1)_forwards]', // collapsing animation
16-
className,
17-
)}
18-
ref={ref}
19-
{...otherProps}
20-
>
24+
<RadixAccordionContent forceMount={forceMount} className={contentClassNames} ref={ref} {...otherProps}>
2125
<div className="px-4 pb-4 pt-1 md:px-6 md:pb-6">{children}</div>
2226
</RadixAccordionContent>
2327
);

src/core/components/tabs/tabsRoot/tabsRoot.stories.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ const reusableStoryComponent = (props: ITabsRootProps) => {
2323
return (
2424
<Tabs.Root {...props}>
2525
<Tabs.List>
26-
<Tabs.Trigger label="Tab 1" value="1" />
27-
<Tabs.Trigger label="Tab 2" value="2" />
28-
<Tabs.Trigger label="Tab 3" value="3" iconRight={IconType.BLOCKCHAIN_BLOCK} />
26+
<Tabs.Trigger label="Default Tab" value="1" />
27+
<Tabs.Trigger label="Disabled Tab" value="2" disabled={true} />
28+
<Tabs.Trigger label="Icon Tab" value="3" iconRight={IconType.BLOCKCHAIN_BLOCK} />
2929
</Tabs.List>
3030
<Tabs.Content value="1">
3131
<div className="flex h-24 w-96 items-center justify-center border border-dashed border-info-300 bg-info-100">
@@ -66,7 +66,7 @@ export const Underlined: Story = {
6666
* Usage example of a Tabs component inside a Card component with the defaultValue set.
6767
*/
6868
export const InsideCard: Story = {
69-
args: { defaultValue: '2' },
69+
args: { defaultValue: '3' },
7070
render: (args) => <Card className="p-6">{reusableStoryComponent(args)}</Card>,
7171
};
7272

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2-
import { Tabs, type ITabsTriggerProps } from '..';
2+
import type { ComponentType } from 'react';
3+
import { Tabs } from '..';
4+
import { IconType } from '../../icon';
5+
6+
const ComponentWrapper = (Story: ComponentType) => (
7+
<Tabs.Root>
8+
<Tabs.List>
9+
<Story />
10+
</Tabs.List>
11+
</Tabs.Root>
12+
);
313

4-
/**
5-
* 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.
6-
*/
714
const meta: Meta<typeof Tabs.Trigger> = {
815
title: 'Core/Components/Tabs/Tabs.Trigger',
916
component: Tabs.Trigger,
17+
decorators: ComponentWrapper,
1018
parameters: {
1119
design: {
1220
type: 'figma',
@@ -17,25 +25,26 @@ const meta: Meta<typeof Tabs.Trigger> = {
1725

1826
type Story = StoryObj<typeof Tabs.Trigger>;
1927

20-
const reusableStoryComponent = (props: ITabsTriggerProps) => {
21-
return (
22-
<Tabs.Root>
23-
<Tabs.List>
24-
<Tabs.Trigger {...props} />
25-
</Tabs.List>
26-
</Tabs.Root>
27-
);
28+
/**
29+
* Default usage example of a single Tabs.Trigger component.
30+
*/
31+
export const Default: Story = {
32+
args: {
33+
label: 'Example',
34+
value: 'example',
35+
},
2836
};
2937

3038
/**
3139
* Default usage example of a single Tabs.Trigger component.
3240
*/
33-
export const Default: Story = {
41+
export const Disabled: Story = {
3442
args: {
35-
label: 'Tab 1',
36-
value: '1',
43+
label: 'Disabled tab',
44+
value: 'disabled',
45+
iconRight: IconType.APP_ASSETS,
46+
disabled: true,
3747
},
38-
render: (args) => reusableStoryComponent(args),
3948
};
4049

4150
export default meta;

src/core/components/tabs/tabsTrigger/tabsTrigger.test.tsx

+13-15
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,38 @@ import { IconType } from '../../icon';
33
import { Tabs, type ITabsTriggerProps } from '../../tabs';
44

55
describe('<Tabs.Trigger /> component', () => {
6-
const createTestComponent = (props?: Partial<ITabsTriggerProps>, isUnderlined?: boolean) => {
6+
const createTestComponent = (props?: Partial<ITabsTriggerProps>) => {
77
const completeProps: ITabsTriggerProps = {
88
label: 'Tab 1',
99
value: '1',
1010
...props,
1111
};
1212

1313
return (
14-
<Tabs.Root isUnderlined={isUnderlined}>
14+
<Tabs.Root>
1515
<Tabs.List>
1616
<Tabs.Trigger {...completeProps} />
1717
</Tabs.List>
1818
</Tabs.Root>
1919
);
2020
};
2121

22-
it('should render without crashing', () => {
22+
it('renders a tab', () => {
2323
render(createTestComponent());
24-
25-
expect(screen.getByRole('tab')).toBeInTheDocument();
24+
const tab = screen.getByRole('tab');
25+
expect(tab).toBeInTheDocument();
26+
expect(tab.getAttribute('disabled')).toBeNull();
2627
});
2728

28-
it('should pass the correct value prop', () => {
29-
const value = 'complex1';
30-
render(createTestComponent({ value }));
31-
32-
const triggerElement = screen.getByRole('tab');
33-
expect(triggerElement).toHaveAttribute('id', `radix-:r2:-trigger-${value}`);
34-
});
35-
36-
it('should render the icon when iconRight is provided', () => {
29+
it('renders the icon when iconRight is provided', () => {
3730
const iconRight = IconType.BLOCKCHAIN_BLOCK;
3831
render(createTestComponent({ iconRight }));
32+
expect(screen.getByTestId(iconRight)).toBeInTheDocument();
33+
});
3934

40-
expect(screen.getByTestId('BLOCKCHAIN_BLOCK')).toBeInTheDocument();
35+
it('disables the tab when the disabled property is set to true', () => {
36+
const disabled = true;
37+
render(createTestComponent({ disabled }));
38+
expect(screen.getByRole('tab').getAttribute('disabled')).toEqual('');
4139
});
4240
});

src/core/components/tabs/tabsTrigger/tabsTrigger.tsx

+13-13
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,29 @@ export interface ITabsTriggerProps extends ComponentProps<'button'> {
2020
}
2121

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

2626
const triggerClassNames = classNames(
27-
'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
28-
'hover:text-neutral-800', // hover
29-
'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
30-
'focus:outline-none', // focus -- might need style updates pending conversation
31-
{ 'hover:shadow-[inset_0_0_0_0,0_1px_0_0] hover:shadow-neutral-800': isUnderlined }, // isUnderlined variant
32-
'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
27+
'group line-clamp-1 flex items-center gap-x-4 rounded-t border-primary-400 py-3 text-base font-normal leading-tight', // Base
28+
'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
29+
'focus:outline-none', // Focus state
30+
{ 'hover:shadow-[inset_0_0_0_0,0_1px_0_0] hover:shadow-neutral-800': isUnderlined && !disabled }, // Underlined & enabled variant
31+
'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
32+
{ 'cursor-pointer text-neutral-500 hover:text-neutral-800': !disabled }, // Enabled state
33+
{ 'text-neutral-300': disabled }, // Disabled state
3334
className,
3435
);
3536

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

4444
return (
45-
<RadixTabsTrigger className={triggerClassNames} value={value} {...otherProps}>
45+
<RadixTabsTrigger className={triggerClassNames} value={value} disabled={disabled} {...otherProps}>
4646
{label}
4747
{iconRight && <Icon icon={iconRight} size="sm" className={iconClassNames} />}
4848
</RadixTabsTrigger>
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { IProposalActionsActionRawViewProps, ProposalActionsActionRawView } from './proposalActionsActionRawView';
1+
export { ProposalActionsActionRawView, type IProposalActionsActionRawViewProps } from './proposalActionsActionRawView';

0 commit comments

Comments
 (0)