Skip to content

Commit d4415a2

Browse files
author
Lucas
authored
[LW-9151] Edit account name component (#746)
* feat(ui): input component * feat(extension): edit account drawer * feat(extension): add unit test * feat: rename input component * feat(ui): use pseudo parameters * feat(ui): input width * feat(ui): input disabled style * feat(extension): add ui package * feat: update lock file * feat(ui): remove redundant styles
1 parent c9af7bc commit d4415a2

File tree

11 files changed

+367
-0
lines changed

11 files changed

+367
-0
lines changed

apps/browser-extension-wallet/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@lace/common": "0.1.0",
5555
"@lace/core": "0.1.0",
5656
"@lace/staking": "0.1.0",
57+
"@lace/ui": "^0.1.0",
5758
"@react-rxjs/core": "^0.9.8",
5859
"@react-rxjs/utils": "^0.9.5",
5960
"@vespaiach/axios-fetch-adapter": "^0.3.0",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { render, fireEvent, screen } from '@testing-library/react';
3+
import { EditAccountDrawer } from './EditAccountDrawer';
4+
import '@testing-library/jest-dom';
5+
6+
jest.mock('react-i18next', () => ({
7+
useTranslation: () => ({ t: jest.fn() })
8+
}));
9+
10+
describe('EditAccountDrawer', () => {
11+
const onSaveMock = jest.fn();
12+
const hideMock = jest.fn();
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('displays default account name', () => {
19+
render(<EditAccountDrawer name="" index="1" visible onSave={onSaveMock} hide={hideMock} />);
20+
21+
expect(screen.getByTestId('edit-account')).toBeInTheDocument();
22+
expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Account #1');
23+
expect(screen.getByTestId('edit-account-name-input')).toHaveValue('');
24+
expect(screen.getByTestId('edit-account-save-btn')).toBeDisabled();
25+
});
26+
27+
it('displays correct account name', () => {
28+
render(<EditAccountDrawer name="Test Account" index="1" visible onSave={onSaveMock} hide={hideMock} />);
29+
30+
expect(screen.getByTestId('edit-account')).toBeInTheDocument();
31+
expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Test Account');
32+
expect(screen.getByTestId('edit-account-name-input')).toHaveValue('Test Account');
33+
expect(screen.getByTestId('edit-account-save-btn')).toBeDisabled();
34+
});
35+
36+
it('updates input value on change and enables save button', () => {
37+
render(<EditAccountDrawer name="Test Account" index="1" visible onSave={onSaveMock} hide={hideMock} />);
38+
39+
const input = screen.getByTestId('edit-account-name-input');
40+
41+
fireEvent.change(input, { target: { value: 'New Account Name' } });
42+
fireEvent.click(screen.getByTestId('edit-account-save-btn'));
43+
44+
expect(input).toHaveValue('New Account Name');
45+
expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Test Account');
46+
expect(screen.getByTestId('edit-account-save-btn')).toBeEnabled();
47+
expect(onSaveMock).toHaveBeenCalledWith('New Account Name');
48+
});
49+
50+
it('calls hide function when Cancel button is clicked', () => {
51+
render(<EditAccountDrawer name="Test Account" index="1" visible onSave={onSaveMock} hide={hideMock} />);
52+
53+
fireEvent.click(screen.getByTestId('edit-account-cancel-btn'));
54+
55+
expect(hideMock).toHaveBeenCalled();
56+
});
57+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Box, Flex, Button, Text, TextBox } from '@lace/ui';
4+
import { Drawer, DrawerNavigation } from '@lace/common';
5+
6+
export type Props = {
7+
onSave: (name: string) => void;
8+
visible: boolean;
9+
hide: () => void;
10+
name: string;
11+
index: string;
12+
};
13+
14+
export const EditAccountDrawer = ({ name, index, visible, onSave, hide }: Props): React.ReactElement => {
15+
const { t } = useTranslation();
16+
const [currentName, setCurrentName] = useState(name);
17+
18+
return (
19+
<Drawer
20+
zIndex={999}
21+
open={visible}
22+
navigation={<DrawerNavigation title={name || `Account #${index}`} onCloseIconClick={hide} />}
23+
footer={
24+
<Flex flexDirection="column">
25+
<Box mb="$16" w="$fill">
26+
<Button.CallToAction
27+
w="$fill"
28+
disabled={name === currentName}
29+
onClick={() => onSave(currentName)}
30+
data-testid="edit-account-save-btn"
31+
label={t('account.edit.footer.save')}
32+
/>
33+
</Box>
34+
<Button.Secondary
35+
w="$fill"
36+
onClick={hide}
37+
data-testid="edit-account-cancel-btn"
38+
label={t('account.edit.footer.cancel')}
39+
/>
40+
</Flex>
41+
}
42+
>
43+
<div data-testid="edit-account">
44+
<Box mb="$16">
45+
<Text.SubHeading weight="$bold">{t('account.edit.title')}</Text.SubHeading>
46+
</Box>
47+
<Box mb="$64">
48+
<Text.Body.Normal>{t('account.edit.subtitle')}</Text.Body.Normal>
49+
</Box>
50+
<TextBox
51+
data-testid="edit-account-name-input"
52+
containerStyle={{ width: '100%' }}
53+
label={t('account.edit.input.label')}
54+
value={currentName}
55+
onChange={(e) => setCurrentName(e.target.value)}
56+
/>
57+
</div>
58+
</Drawer>
59+
);
60+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useState } from 'react';
2+
3+
export const useEditAccountDrawer = (): { isOpen: boolean; open: () => void; hide: () => void } => {
4+
const [visible, setVisible] = useState(false);
5+
6+
return {
7+
isOpen: visible,
8+
open: () => setVisible(true),
9+
hide: () => setVisible(false)
10+
};
11+
};

apps/browser-extension-wallet/src/lib/translations/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,5 +1313,14 @@
13131313
"errorText": "Wallet failed to sync",
13141314
"successText": "Wallet synced successfully"
13151315
}
1316+
},
1317+
"account": {
1318+
"edit": {
1319+
"title": "Edit account name",
1320+
"subtitle": "Choose a name to identify your account",
1321+
"input.label": "Account name",
1322+
"footer.save": "Save",
1323+
"footer.cancel": "Cancel"
1324+
}
13161325
}
13171326
}

packages/ui/src/design-system/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export { PasswordBox } from './password-box';
2727
export { Metadata } from './metadata';
2828
export { TextLink } from './text-link';
2929
export * as ProfileDropdown from './profile-dropdown';
30+
export { TextBox } from './text-box';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { TextBox } from './text-box.component';
2+
export type { TextBoxProps } from './text-box.component';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
3+
import * as Form from '@radix-ui/react-form';
4+
import cn from 'classnames';
5+
6+
import { Flex } from '../flex';
7+
import * as Typography from '../typography';
8+
9+
import * as cx from './text-box.css';
10+
11+
export interface TextBoxProps extends Form.FormControlProps {
12+
required?: boolean;
13+
disabled?: boolean;
14+
id?: string;
15+
label: string;
16+
name?: string;
17+
value: string;
18+
errorMessage?: string;
19+
onChange?: (event: Readonly<React.ChangeEvent<HTMLInputElement>>) => void;
20+
containerClassName?: string;
21+
containerStyle?: React.CSSProperties;
22+
'data-testid'?: string;
23+
}
24+
25+
export const TextBox = ({
26+
required = false,
27+
disabled = false,
28+
id,
29+
label,
30+
name,
31+
value,
32+
errorMessage = '',
33+
containerClassName = '',
34+
onChange,
35+
containerStyle,
36+
...rest
37+
}: Readonly<TextBoxProps>): JSX.Element => {
38+
return (
39+
<Form.Root>
40+
<Flex justifyContent="space-between" alignItems="center">
41+
<Form.Field
42+
name="field"
43+
className={cn(cx.container, {
44+
[containerClassName]: containerClassName,
45+
})}
46+
style={containerStyle}
47+
>
48+
<Form.Control asChild>
49+
<input
50+
type="text"
51+
required={required}
52+
placeholder=""
53+
className={cx.input}
54+
disabled={disabled}
55+
name={name}
56+
value={value}
57+
onChange={onChange}
58+
id={id}
59+
data-testid={rest['data-testid']}
60+
/>
61+
</Form.Control>
62+
<Form.Label className={cn(cx.label)}>{label}</Form.Label>
63+
</Form.Field>
64+
</Flex>
65+
{errorMessage && (
66+
<Typography.Label className={cx.errorMessage}>
67+
{errorMessage}
68+
</Typography.Label>
69+
)}
70+
</Form.Root>
71+
);
72+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { globalStyle, vars, style } from '../../design-tokens';
2+
3+
export const container = style({
4+
background: vars.colors.$input_container_bgColor,
5+
paddingTop: vars.spacing.$12,
6+
maxHeight: vars.spacing.$52,
7+
borderRadius: vars.radius.$medium,
8+
width: 'auto',
9+
fontWeight: vars.fontWeights.$semibold,
10+
fontFamily: vars.fontFamily.$nova,
11+
});
12+
13+
export const input = style({
14+
width: 'calc(100% - 90px)',
15+
fontSize: vars.fontSizes.$18,
16+
padding: `${vars.spacing.$10} ${vars.spacing.$20}`,
17+
border: 'none',
18+
outline: 'none',
19+
background: 'transparent',
20+
color: vars.colors.$input_value_color,
21+
});
22+
23+
export const label = style({
24+
position: 'relative',
25+
display: 'block',
26+
left: vars.spacing.$20,
27+
top: '-40px',
28+
transitionDuration: '0.2s',
29+
pointerEvents: 'none',
30+
color: vars.colors.$input_label_color,
31+
fontSize: vars.fontSizes.$18,
32+
});
33+
34+
export const errorMessage = style({
35+
color: vars.colors.$input_error_message_color,
36+
marginLeft: vars.spacing.$20,
37+
});
38+
39+
globalStyle(
40+
`${input}:focus + ${label}, ${input}:not(:placeholder-shown) + ${label}`,
41+
{
42+
top: '-52px',
43+
fontSize: vars.fontSizes.$12,
44+
},
45+
);
46+
47+
globalStyle(`${container}:has(${input}:disabled)`, {
48+
opacity: vars.opacities.$0_5,
49+
});
50+
51+
globalStyle(`${container}:has(${input}:hover:not(:disabled))`, {
52+
outline: `2px solid ${vars.colors.$input_container_hover_outline_color}`,
53+
});
54+
55+
globalStyle(`${container}:has(${input}:focus)`, {
56+
outline: `3px solid ${vars.colors.$input_container_focused_outline_color}`,
57+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from 'react';
2+
3+
import type { Meta } from '@storybook/react';
4+
5+
import { LocalThemeProvider, ThemeColorScheme } from '../../design-tokens';
6+
import { page, Section, Variants } from '../decorators';
7+
import { Divider } from '../divider';
8+
import { Flex } from '../flex';
9+
import { Cell, Grid } from '../grid';
10+
11+
import { TextBox } from './text-box.component';
12+
13+
export default {
14+
title: 'Input Fields/Text Box',
15+
component: TextBox,
16+
decorators: [
17+
page({
18+
title: 'Text Box',
19+
subtitle: 'A text input',
20+
}),
21+
],
22+
} as Meta;
23+
24+
const MainComponents = (): JSX.Element => (
25+
<Variants.Row>
26+
<Variants.Cell>
27+
<TextBox label="Label" value="" />
28+
</Variants.Cell>
29+
<Variants.Cell>
30+
<TextBox label="Label" value="" id="hover" />
31+
</Variants.Cell>
32+
<Variants.Cell>
33+
<TextBox label="Label" value="Input Text" />
34+
</Variants.Cell>
35+
<Variants.Cell>
36+
<TextBox label="Label" value="Input Text" errorMessage="Error" />
37+
</Variants.Cell>
38+
<Variants.Cell>
39+
<TextBox label="Label" value="Input Text" disabled />
40+
</Variants.Cell>
41+
<Variants.Cell>
42+
<TextBox label="Label" value="" id="focus" />
43+
</Variants.Cell>
44+
</Variants.Row>
45+
);
46+
47+
export const Overview = (): JSX.Element => {
48+
const [value, setValue] = React.useState('');
49+
50+
return (
51+
<Grid>
52+
<Cell>
53+
<Section title="Copy for use">
54+
<Flex flexDirection="column" alignItems="center" w="$fill" my="$32">
55+
<TextBox
56+
value={value}
57+
label="Text"
58+
onChange={(event): void => {
59+
setValue(event.target.value);
60+
}}
61+
/>
62+
</Flex>
63+
</Section>
64+
65+
<Divider my="$64" />
66+
67+
<Section title="Main components">
68+
<Variants.Table
69+
headers={[
70+
'Rest',
71+
'Hover',
72+
'Active/Pressed',
73+
'Error',
74+
'Disabled',
75+
'Focused',
76+
]}
77+
>
78+
<MainComponents />
79+
</Variants.Table>
80+
<LocalThemeProvider colorScheme={ThemeColorScheme.Dark}>
81+
<Variants.Table>
82+
<MainComponents />
83+
</Variants.Table>
84+
</LocalThemeProvider>
85+
</Section>
86+
</Cell>
87+
</Grid>
88+
);
89+
};
90+
91+
Overview.parameters = {
92+
pseudo: {
93+
hover: '#hover',
94+
focus: '#focus',
95+
},
96+
};

0 commit comments

Comments
 (0)