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

Fix: Multisig select #2626

Merged
merged 16 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@radix-ui/react-dropdown-menu": "2.1.2",
"@radix-ui/react-popover": "1.1.2",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "2.1.2",
"@radix-ui/react-slider": "1.2.1",
"@radix-ui/react-tooltip": "1.1.3",
"@react-spring/web": "9.7.5",
Expand Down
45 changes: 45 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions src/renderer/shared/ui-kit/Field/Field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type Meta, type StoryObj } from '@storybook/react';

import { Field } from './Field';

const meta: Meta<typeof Field> = {
title: 'Design System/kit/Field',
component: Field,
};

export default meta;

type Story = StoryObj<typeof Field>;

export const Default: Story = {
tuul-wq marked this conversation as resolved.
Show resolved Hide resolved
args: {
text: 'Label name',
children: <input value="Input value" className="w-52 rounded border border-gray-300 p-2" />,
},
};
14 changes: 14 additions & 0 deletions src/renderer/shared/ui-kit/Field/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type PropsWithChildren } from 'react';

type Props = {
text: string;
};

export const Field = ({ text, children }: PropsWithChildren<Props>) => {
return (
<label className="flex w-full flex-col gap-y-2">
<span className="text-footnote font-medium text-text-tertiary">{text}</span>
{children}
</label>
);
};
62 changes: 62 additions & 0 deletions src/renderer/shared/ui-kit/Select/Select.stories.tsx
tuul-wq marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { useState } from 'react';

import { Box } from '../Box/Box';

import { Select } from './Select';

const meta: Meta<typeof Select> = {
title: 'Design System/kit/Select',
component: Select,
parameters: {
layout: 'centered',
},
render: (params) => {
const [value, onChange] = useState('');

return (
<Box width="200px">
<Select {...params} placeholder="Select a fruit" value={value} onChange={onChange}>
<Select.Content>
<Select.Item value="item_1">Apple</Select.Item>
<Select.Item value="item_2">Orange</Select.Item>
<Select.Item value="item_3">Watermelon</Select.Item>
<Select.Item value="item_4">Banana</Select.Item>
</Select.Content>
</Select>
</Box>
);
},
};

export default meta;

type Story = StoryObj<typeof Select>;

export const Default: Story = {};

export const Invalid: Story = {
args: {
invalid: true,
},
};

export const Disabled: Story = {
args: {
disabled: true,
},
};

export const Dark: Story = {
decorators: [
(Story, { args }) => {
const [open, onToggle] = useState(false);

return (
<div className="flex h-[300px] w-[400px] justify-center rounded-lg bg-black pt-[50px]">
<Story args={{ ...args, open, onToggle, theme: 'dark' }} />;
</div>
);
},
],
};
151 changes: 151 additions & 0 deletions src/renderer/shared/ui-kit/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as RadixSelect from '@radix-ui/react-select';
import { type PropsWithChildren, createContext, useContext, useMemo } from 'react';

import { type XOR } from '@/shared/core';
import { cnTw } from '@/shared/lib/utils';
import { Icon } from '@/shared/ui';
import { ScrollArea } from '../ScrollArea/ScrollArea';
import { Surface } from '../Surface/Surface';
import { useTheme } from '../Theme/useTheme';
import { gridSpaceConverter } from '../_helpers/gridSpaceConverter';

type ContextProps = {
theme?: 'light' | 'dark';
johnthecat marked this conversation as resolved.
Show resolved Hide resolved
invalid?: boolean;
disabled?: boolean;
testId?: string;
};

const Context = createContext<ContextProps>({});

type ControlledSelectProps = {
placeholder: string;
value: string;
onChange: (value: string) => void;
} & XOR<{
open: boolean;
onToggle: (value: boolean) => void;
}>;

type RootProps = PropsWithChildren<ControlledSelectProps & ContextProps>;

const Root = ({
theme = 'light',
invalid,
disabled,
testId = 'Select',
open,
onToggle,
placeholder,
value,
onChange,
children,
}: RootProps) => {
const ctx = useMemo(() => ({ theme, invalid, disabled, testId }), [theme, invalid, disabled, testId]);

return (
<Context.Provider value={ctx}>
<RadixSelect.Root open={open} disabled={disabled} value={value} onOpenChange={onToggle} onValueChange={onChange}>
<Button placeholder={placeholder} />
{children}
</RadixSelect.Root>
</Context.Provider>
);
};

type TriggerProps = {
placeholder: string;
};

const Button = ({ placeholder }: TriggerProps) => {
const { theme, invalid, disabled } = useContext(Context);

return (
<RadixSelect.Trigger
className={cnTw(
'w-full px-[11px] py-[7px]',
'rounded border text-footnote outline-offset-1',
'enabled:hover:shadow-card-shadow',
'data-[state=open]:border-active-container-border',
{
'border-filter-border bg-input-background text-text-primary': theme === 'light',
'border-border-dark text-white': theme === 'dark',
'bg-input-background-disabled text-text-tertiary': disabled,
'border-filter-border-negative': invalid,
},
)}
>
<div className="flex items-center justify-between">
<RadixSelect.Value placeholder={placeholder} />
<Icon name="down" size={16} className="shrink-0" />
</div>
</RadixSelect.Trigger>
);
};

const Content = ({ children }: PropsWithChildren) => {
const { portalContainer } = useTheme();
const { testId, theme } = useContext(Context);

return (
<RadixSelect.Portal container={portalContainer}>
<RadixSelect.Content
asChild
position="popper"
avoidCollisions={false}
collisionPadding={gridSpaceConverter(2)}
sideOffset={gridSpaceConverter(2)}
style={{ width: 'var(--radix-select-trigger-width)' }}
data-testid={testId}
>
<Surface
elevation={1}
className={cnTw(
'z-50 flex flex-col',
'h-max max-h-[--radix-popper-available-height] max-w-60',
'min-w-20 overflow-hidden duration-100 animate-in fade-in zoom-in-95',
{
'border-border-dark bg-background-dark': theme === 'dark',
},
)}
>
<ScrollArea>
<RadixSelect.Viewport asChild>
<div className="flex flex-col gap-y-1 p-1">{children}</div>
</RadixSelect.Viewport>
</ScrollArea>
</Surface>
</RadixSelect.Content>
</RadixSelect.Portal>
);
};

type ItemProps = {
value: string;
};

const Item = ({ value, children }: PropsWithChildren<ItemProps>) => {
const { theme } = useContext(Context);

return (
<RadixSelect.Item
value={value}
className={cnTw(
'flex cursor-pointer rounded p-2 text-footnote text-text-secondary',
'focus:bg-action-background-hover focus:outline-none data-[highlighted]:bg-action-background-hover',
{
'focus:bg-block-background-hover data-[highlighted]:bg-background-item-hover': theme === 'dark',
},
)}
>
<RadixSelect.ItemText asChild>
<div className="h-full w-full">{children}</div>
</RadixSelect.ItemText>
</RadixSelect.Item>
);
};

export const Select = Object.assign(Root, {
Content,
Item,
johnthecat marked this conversation as resolved.
Show resolved Hide resolved
});
2 changes: 2 additions & 0 deletions src/renderer/shared/ui-kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export { Carousel } from './Carousel/Carousel';
export { Tooltip } from './Tooltip/Tooltip';
export { Popover } from './Popover/Popover';
export { Surface } from './Surface/Surface';
export { Select } from './Select/Select';
export { Slider } from './Slider/Slider';
export { Label } from './Label/Label';
export { Modal } from './Modal/Modal';
export { Field } from './Field/Field';
export { Box } from './Box/Box';
4 changes: 2 additions & 2 deletions src/renderer/widgets/CreateWallet/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Account, type Chain, type Transaction } from '@/shared/core';
import { type Account, type ChainId, type Transaction } from '@/shared/core';

export const enum Step {
NAME_NETWORK,
Expand All @@ -11,7 +11,7 @@ export const enum Step {

export type FormParams = {
threshold: number;
chain: Chain;
chainId: ChainId;
name: string;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { allSettled, fork } from 'effector';

import { type Account, type Chain } from '@/shared/core';
import { type Account, type ChainId } from '@/shared/core';
import { networkModel } from '@/entities/network';
import { walletModel } from '@/entities/wallet';
import { confirmModel } from '../confirm-model';
Expand All @@ -20,7 +20,7 @@ describe('widgets/CreateWallet/model/confirm-model', () => {
});

const store = {
chain: { chainId: '0x00' } as unknown as Chain,
chainId: '0x00' as ChainId,
account: { walletId: signerWallet.id } as unknown as Account,
signer: { walletId: signerWallet.id } as unknown as Account,
threshold: 2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { allSettled, fork } from 'effector';

import { type Account, type Chain, ConnectionStatus } from '@/shared/core';
import { type Account, type ChainId, ConnectionStatus } from '@/shared/core';
import { toAddress } from '@/shared/lib/utils';
import { networkModel } from '@/entities/network';
import { walletModel } from '@/entities/wallet';
Expand Down Expand Up @@ -55,14 +55,14 @@ describe('widgets/CreateWallet/model/form-model', () => {
await allSettled(flowModel.events.signerSelected, { scope, params: signerWallet.accounts[0].accountId });

expect(scope.getState(flowModel.$step)).toEqual(Step.NAME_NETWORK);
await allSettled(formModel.$createMultisigForm.fields.chain.onChange, { scope, params: testChain });
await allSettled(formModel.$createMultisigForm.fields.chainId.onChange, { scope, params: testChain.chainId });
await allSettled(formModel.$createMultisigForm.fields.name.onChange, { scope, params: 'some name' });
await allSettled(formModel.$createMultisigForm.fields.threshold.onChange, { scope, params: 2 });

await allSettled(formModel.$createMultisigForm.submit, { scope });

const store = {
chain: { chainId: '0x00' } as unknown as Chain,
chainId: '0x00' as ChainId,
account: { walletId: signerWallet.id } as unknown as Account,
signer: { walletId: signerWallet.id } as unknown as Account,
threshold: 2,
Expand Down
Loading
Loading