diff --git a/apps/browser-extension-wallet/package.json b/apps/browser-extension-wallet/package.json index 7b93f63baf..22f390146c 100644 --- a/apps/browser-extension-wallet/package.json +++ b/apps/browser-extension-wallet/package.json @@ -54,6 +54,7 @@ "@lace/common": "0.1.0", "@lace/core": "0.1.0", "@lace/staking": "0.1.0", + "@lace/ui": "^0.1.0", "@react-rxjs/core": "^0.9.8", "@react-rxjs/utils": "^0.9.5", "@vespaiach/axios-fetch-adapter": "^0.3.0", diff --git a/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.test.tsx b/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.test.tsx new file mode 100644 index 0000000000..7f06c9d6d2 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { EditAccountDrawer } from './EditAccountDrawer'; +import '@testing-library/jest-dom'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: jest.fn() }) +})); + +describe('EditAccountDrawer', () => { + const onSaveMock = jest.fn(); + const hideMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays default account name', () => { + render(); + + expect(screen.getByTestId('edit-account')).toBeInTheDocument(); + expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Account #1'); + expect(screen.getByTestId('edit-account-name-input')).toHaveValue(''); + expect(screen.getByTestId('edit-account-save-btn')).toBeDisabled(); + }); + + it('displays correct account name', () => { + render(); + + expect(screen.getByTestId('edit-account')).toBeInTheDocument(); + expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Test Account'); + expect(screen.getByTestId('edit-account-name-input')).toHaveValue('Test Account'); + expect(screen.getByTestId('edit-account-save-btn')).toBeDisabled(); + }); + + it('updates input value on change and enables save button', () => { + render(); + + const input = screen.getByTestId('edit-account-name-input'); + + fireEvent.change(input, { target: { value: 'New Account Name' } }); + fireEvent.click(screen.getByTestId('edit-account-save-btn')); + + expect(input).toHaveValue('New Account Name'); + expect(screen.getByTestId('drawer-navigation-title')).toHaveTextContent('Test Account'); + expect(screen.getByTestId('edit-account-save-btn')).toBeEnabled(); + expect(onSaveMock).toHaveBeenCalledWith('New Account Name'); + }); + + it('calls hide function when Cancel button is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('edit-account-cancel-btn')); + + expect(hideMock).toHaveBeenCalled(); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.tsx b/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.tsx new file mode 100644 index 0000000000..1e1f27b568 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/account/components/EditAccount/EditAccountDrawer.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Flex, Button, Text, TextBox } from '@lace/ui'; +import { Drawer, DrawerNavigation } from '@lace/common'; + +export type Props = { + onSave: (name: string) => void; + visible: boolean; + hide: () => void; + name: string; + index: string; +}; + +export const EditAccountDrawer = ({ name, index, visible, onSave, hide }: Props): React.ReactElement => { + const { t } = useTranslation(); + const [currentName, setCurrentName] = useState(name); + + return ( + } + footer={ + + + onSave(currentName)} + data-testid="edit-account-save-btn" + label={t('account.edit.footer.save')} + /> + + + + } + > +
+ + {t('account.edit.title')} + + + {t('account.edit.subtitle')} + + setCurrentName(e.target.value)} + /> +
+
+ ); +}; diff --git a/apps/browser-extension-wallet/src/features/account/components/EditAccount/hooks.ts b/apps/browser-extension-wallet/src/features/account/components/EditAccount/hooks.ts new file mode 100644 index 0000000000..f17ee3d5f8 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/account/components/EditAccount/hooks.ts @@ -0,0 +1,11 @@ +import { useState } from 'react'; + +export const useEditAccountDrawer = (): { isOpen: boolean; open: () => void; hide: () => void } => { + const [visible, setVisible] = useState(false); + + return { + isOpen: visible, + open: () => setVisible(true), + hide: () => setVisible(false) + }; +}; diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index a383db68f8..6b60b78510 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1313,5 +1313,14 @@ "errorText": "Wallet failed to sync", "successText": "Wallet synced successfully" } + }, + "account": { + "edit": { + "title": "Edit account name", + "subtitle": "Choose a name to identify your account", + "input.label": "Account name", + "footer.save": "Save", + "footer.cancel": "Cancel" + } } } diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index f865c3f124..e4c37ec074 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -27,3 +27,4 @@ export { PasswordBox } from './password-box'; export { Metadata } from './metadata'; export { TextLink } from './text-link'; export * as ProfileDropdown from './profile-dropdown'; +export { TextBox } from './text-box'; diff --git a/packages/ui/src/design-system/text-box/index.ts b/packages/ui/src/design-system/text-box/index.ts new file mode 100644 index 0000000000..65e82881ee --- /dev/null +++ b/packages/ui/src/design-system/text-box/index.ts @@ -0,0 +1,2 @@ +export { TextBox } from './text-box.component'; +export type { TextBoxProps } from './text-box.component'; diff --git a/packages/ui/src/design-system/text-box/text-box.component.tsx b/packages/ui/src/design-system/text-box/text-box.component.tsx new file mode 100644 index 0000000000..4103d53e4a --- /dev/null +++ b/packages/ui/src/design-system/text-box/text-box.component.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import * as Form from '@radix-ui/react-form'; +import cn from 'classnames'; + +import { Flex } from '../flex'; +import * as Typography from '../typography'; + +import * as cx from './text-box.css'; + +export interface TextBoxProps extends Form.FormControlProps { + required?: boolean; + disabled?: boolean; + id?: string; + label: string; + name?: string; + value: string; + errorMessage?: string; + onChange?: (event: Readonly>) => void; + containerClassName?: string; + containerStyle?: React.CSSProperties; + 'data-testid'?: string; +} + +export const TextBox = ({ + required = false, + disabled = false, + id, + label, + name, + value, + errorMessage = '', + containerClassName = '', + onChange, + containerStyle, + ...rest +}: Readonly): JSX.Element => { + return ( + + + + + + + {label} + + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; diff --git a/packages/ui/src/design-system/text-box/text-box.css.ts b/packages/ui/src/design-system/text-box/text-box.css.ts new file mode 100644 index 0000000000..9743bda7c3 --- /dev/null +++ b/packages/ui/src/design-system/text-box/text-box.css.ts @@ -0,0 +1,57 @@ +import { globalStyle, vars, style } from '../../design-tokens'; + +export const container = style({ + background: vars.colors.$input_container_bgColor, + paddingTop: vars.spacing.$12, + maxHeight: vars.spacing.$52, + borderRadius: vars.radius.$medium, + width: 'auto', + fontWeight: vars.fontWeights.$semibold, + fontFamily: vars.fontFamily.$nova, +}); + +export const input = style({ + width: 'calc(100% - 90px)', + fontSize: vars.fontSizes.$18, + padding: `${vars.spacing.$10} ${vars.spacing.$20}`, + border: 'none', + outline: 'none', + background: 'transparent', + color: vars.colors.$input_value_color, +}); + +export const label = style({ + position: 'relative', + display: 'block', + left: vars.spacing.$20, + top: '-40px', + transitionDuration: '0.2s', + pointerEvents: 'none', + color: vars.colors.$input_label_color, + fontSize: vars.fontSizes.$18, +}); + +export const errorMessage = style({ + color: vars.colors.$input_error_message_color, + marginLeft: vars.spacing.$20, +}); + +globalStyle( + `${input}:focus + ${label}, ${input}:not(:placeholder-shown) + ${label}`, + { + top: '-52px', + fontSize: vars.fontSizes.$12, + }, +); + +globalStyle(`${container}:has(${input}:disabled)`, { + opacity: vars.opacities.$0_5, +}); + +globalStyle(`${container}:has(${input}:hover:not(:disabled))`, { + outline: `2px solid ${vars.colors.$input_container_hover_outline_color}`, +}); + +globalStyle(`${container}:has(${input}:focus)`, { + outline: `3px solid ${vars.colors.$input_container_focused_outline_color}`, +}); diff --git a/packages/ui/src/design-system/text-box/text-box.stories.tsx b/packages/ui/src/design-system/text-box/text-box.stories.tsx new file mode 100644 index 0000000000..eb5236347c --- /dev/null +++ b/packages/ui/src/design-system/text-box/text-box.stories.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import type { Meta } from '@storybook/react'; + +import { LocalThemeProvider, ThemeColorScheme } from '../../design-tokens'; +import { page, Section, Variants } from '../decorators'; +import { Divider } from '../divider'; +import { Flex } from '../flex'; +import { Cell, Grid } from '../grid'; + +import { TextBox } from './text-box.component'; + +export default { + title: 'Input Fields/Text Box', + component: TextBox, + decorators: [ + page({ + title: 'Text Box', + subtitle: 'A text input', + }), + ], +} as Meta; + +const MainComponents = (): JSX.Element => ( + + + + + + + + + + + + + + + + + + + + +); + +export const Overview = (): JSX.Element => { + const [value, setValue] = React.useState(''); + + return ( + + +
+ + { + setValue(event.target.value); + }} + /> + +
+ + + +
+ + + + + + + + +
+
+
+ ); +}; + +Overview.parameters = { + pseudo: { + hover: '#hover', + focus: '#focus', + }, +}; diff --git a/yarn.lock b/yarn.lock index fb109c846a..fda8b47d21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7853,6 +7853,7 @@ __metadata: "@lace/common": 0.1.0 "@lace/core": 0.1.0 "@lace/staking": 0.1.0 + "@lace/ui": ^0.1.0 "@react-rxjs/core": ^0.9.8 "@react-rxjs/utils": ^0.9.5 "@types/dotenv-webpack": 7.0.3