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

feat: APP-2727 - Implement AddressInput module component #123

Merged
merged 41 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
07ae8fa
Create component structure
cgero-eth Mar 7, 2024
30d2e4d
Implement initial functionalities of address-input component
cgero-eth Mar 7, 2024
53f5bb7
Cleanup implementation, handle changing value programmatically
cgero-eth Mar 7, 2024
8e4478c
Implement copy & block-explorer buttons
cgero-eth Mar 8, 2024
dba958e
Update address-input implementation
cgero-eth Mar 8, 2024
03a28c6
Remove truncate from inputs, display copy button whenever there's a v…
cgero-eth Mar 8, 2024
e2dd5b1
Debounce RPC requests
cgero-eth Mar 11, 2024
7e2b3f7
Implement tests for useDebouncedValue hook
cgero-eth Mar 11, 2024
ed3c4cc
Implement tests for clipboard utils
cgero-eth Mar 11, 2024
025a48f
Implement tests for address-utils, correctly setup wagmi for testing
cgero-eth Mar 11, 2024
a4f8f8f
Export useInputProps from core package, start implementation of tests
cgero-eth Mar 11, 2024
cbfc26f
Fix displaying of buttons, check if current chain-id supports ens names
cgero-eth Mar 11, 2024
0db9148
Implement more tests
cgero-eth Mar 12, 2024
13e9c78
Update address and ens utils
cgero-eth Mar 12, 2024
b04f7ea
Truncate long ens names on address-input component
cgero-eth Mar 12, 2024
d92a675
Update addressInput.test.tsx
cgero-eth Mar 12, 2024
5f542b2
Update member-avatar to use address/ens utilities
cgero-eth Mar 12, 2024
ac820fd
Add return valud to copy function
cgero-eth Mar 12, 2024
c37a809
Use Address type instead of Hash
cgero-eth Mar 12, 2024
9405659
Cleanup member-avatar test setup
cgero-eth Mar 12, 2024
6025173
Reset mocks
cgero-eth Mar 12, 2024
044e036
Implement tests for copy and link buttons
cgero-eth Mar 12, 2024
af80437
Use member-avatar component, add getChecksum util to address utils
cgero-eth Mar 13, 2024
e73caa0
Start adding new tests
cgero-eth Mar 13, 2024
55c56ce
Update CHANGELOG.md
cgero-eth Mar 13, 2024
1dca3ca
Add tests
cgero-eth Mar 13, 2024
10a2aaf
Align usage of address and ens utilities
cgero-eth Mar 14, 2024
983fe4f
Use getChecksum instead of getAddress
cgero-eth Mar 14, 2024
cea0f00
Fix isEnsName check
cgero-eth Mar 14, 2024
7c3dc79
Update CHANGELOG.md
cgero-eth Mar 15, 2024
92b9577
Fix typo
cgero-eth Mar 15, 2024
cfae87e
Update truncate functions to display correct char
cgero-eth Mar 15, 2024
8d04c0b
Fix label for address button
cgero-eth Mar 15, 2024
a41b6b1
Fix typo on address-input tests
cgero-eth Mar 15, 2024
5e1dfb1
Remove redundant value check, cleanup tests
cgero-eth Mar 15, 2024
70d9599
Update doc of onChange property
cgero-eth Mar 15, 2024
8ccf632
Reduce isAddress / isEnsName calls
cgero-eth Mar 15, 2024
d6a430a
Update addressInput.stories.tsx
cgero-eth Mar 15, 2024
904b971
Fix address button style
cgero-eth Mar 15, 2024
5e23585
Use onCick property instead of onMouseDown for buttons displayed on i…
cgero-eth Mar 15, 2024
1926787
Update CHANGELOG.md
cgero-eth Mar 19, 2024
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Implement `DaoDataListItem`, `ProposalDataListItem.Structure`, and `MemberDataListItem.Structure` module components
- Implement `DaoDataListItem.Structure`, `ProposalDataListItem.Structure`, `MemberDataListItem.Structure` and
`AddressInput` module components
- Implement `StatePingAnimation` core component
- Implement `addressUtils` and `ensUtils` module utilities
- Implement `useDebouncedValue` core hook and `clipboardUtils` core utility

### Changed

- Update `Tag` component primary variant styling
- Update Eslint rules to align usage of boolean properties
- Update default query-client options to set a stale time greater than 0

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const config = {
'^.+\\.svg$': '<rootDir>/src/core/test/svgTransform.js',
'^.+\\.m?[tj]sx?$': 'ts-jest',
},
transformIgnorePatterns: ['node_modules/(?!(.*\\.mjs$|react-merge-refs))'],
transformIgnorePatterns: ['node_modules/(?!(.*\\.mjs$|react-merge-refs|wagmi|@wagmi))'],
};

module.exports = config;
1 change: 1 addition & 0 deletions src/core/components/input/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './hooks';
export * from './inputContainer';
export * from './inputDate';
export * from './inputFileAvatar';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export interface IInputContainerBaseProps {
*/
wrapperClassName?: string;
/**
* Shortcircuits all the input wrapper classes to pass control to the child component.
* Does not render the default input wrapper when set to true, to be used for using the base input container
* properties (label, helpText, ..) for components without a input wrapper (e.g. file inputs).
*/
cgero-eth marked this conversation as resolved.
Show resolved Hide resolved
useCustomWrapper?: boolean;
}
Expand All @@ -75,7 +76,7 @@ export interface IInputContainerProps extends IInputContainerBaseProps, Omit<Com
export type InputComponentElement = HTMLInputElement | HTMLTextAreaElement;

export interface IInputComponentProps<TElement extends InputComponentElement = HTMLInputElement>
extends Omit<IInputContainerBaseProps, 'children' | 'id' | 'inputLength'>,
extends Omit<IInputContainerBaseProps, 'children' | 'id' | 'inputLength' | 'useCustomWrapper'>,
Omit<InputHTMLAttributes<TElement>, 'type'> {
/**
* Classes for the input element.
Expand Down
3 changes: 1 addition & 2 deletions src/core/components/textAreas/textArea/textArea.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import classNames from 'classnames';
import { forwardRef } from 'react';
import { InputContainer, type IInputComponentProps } from '../../input';
import { useInputProps } from '../../input/hooks';
import { InputContainer, useInputProps, type IInputComponentProps } from '../../input';

export interface ITextAreaProps extends IInputComponentProps<HTMLTextAreaElement> {}

Expand Down
1 change: 1 addition & 0 deletions src/core/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useDebouncedValue';
1 change: 1 addition & 0 deletions src/core/hooks/useDebouncedValue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useDebouncedValue, type IUseDebouncedValueParams, type IUseDebouncedValueResult } from './useDebouncedValue';
34 changes: 34 additions & 0 deletions src/core/hooks/useDebouncedValue/useDebouncedValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { act, renderHook } from '@testing-library/react';
import { useDebouncedValue } from './useDebouncedValue';

describe('useDebouncedValue hook', () => {
beforeEach(() => {
jest.useFakeTimers();
});

it('returns the value initialised to the value property and a function to update the debounced value', () => {
const value = 'test-value';
const { result } = renderHook(() => useDebouncedValue(value));
expect(result.current).toEqual([value, expect.any(Function)]);
});

it('debounces the value updates', () => {
const newValue = 'test';
const { result, rerender } = renderHook((value) => useDebouncedValue(value));
expect(result.current[0]).toBeUndefined();

rerender(newValue);
expect(result.current[0]).toBeUndefined();

act(() => jest.runAllTimers());
expect(result.current[0]).toEqual(newValue);
});

it('the returned setter updates the debounced value', () => {
const newValue = 'my-value';
const { result } = renderHook((value) => useDebouncedValue(value));
expect(result.current[0]).toBeUndefined();
act(() => result.current[1](newValue));
expect(result.current[0]).toEqual(newValue);
});
});
39 changes: 39 additions & 0 deletions src/core/hooks/useDebouncedValue/useDebouncedValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useRef, useState } from 'react';

export interface IUseDebouncedValueParams {
/**
* Debounce time period in milliseconds.
* @default 500
*/
delay?: number;
}

export type IUseDebouncedValueResult<TValue> = [
/**
* Debounced value.
*/
TValue,
/**
* Setter for the debounced value.
*/
(value: TValue) => void,
];

export const useDebouncedValue = <TValue>(
value: TValue,
params: IUseDebouncedValueParams = {},
): IUseDebouncedValueResult<TValue> => {
const { delay } = params;

const [debouncedValue, setDebouncedValue] = useState(value);

const timeoutRef = useRef<NodeJS.Timeout>();

useEffect(() => {
timeoutRef.current = setTimeout(() => setDebouncedValue(value), delay);

return () => clearTimeout(timeoutRef.current);
}, [value, delay]);

return [debouncedValue, setDebouncedValue];
};
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './components';
export * from './hooks';
export * from './types';
export * from './utils';
4 changes: 4 additions & 0 deletions src/core/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import { TextDecoder, TextEncoder } from 'util';
import { testLogger } from './utils';

// Setup test logger
testLogger.setup();

// Globally setup TextEncoder/TextDecoder needed by viem
Object.assign(global, { TextDecoder, TextEncoder });
61 changes: 61 additions & 0 deletions src/core/utils/clipboardUtils/clipboardUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { testLogger } from '../../test';
import { clipboardUtils } from './clipboardUtils';

// Navigator.clipboard object is not defined on Jest by default
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: jest.fn(), readText: jest.fn() },
});

describe('clipboard utils', () => {
describe('copy', () => {
const writeTextMock = jest.spyOn(navigator.clipboard, 'writeText');

afterEach(() => {
writeTextMock.mockReset();
});

it('copies the specified value on the user clipboard', async () => {
const copyValue = 'copy-value';
await clipboardUtils.copy(copyValue);
expect(writeTextMock).toHaveBeenCalledWith(copyValue);
});

it('calls the onError callback on copy error', async () => {
testLogger.suppressErrors();
const onError = jest.fn();
const error = new Error('test-error');
writeTextMock.mockImplementation(() => {
throw error;
});
await clipboardUtils.copy('test', { onError });
expect(onError).toHaveBeenCalledWith(error);
});
});

describe('paste', () => {
const readTextMock = jest.spyOn(navigator.clipboard, 'readText');

afterEach(() => {
readTextMock.mockReset();
});

it('reads and returns the user clipboard', async () => {
const clipboardValue = 'test-value';
readTextMock.mockResolvedValue(clipboardValue);
const result = await clipboardUtils.paste();
expect(result).toEqual(clipboardValue);
});

it('calls the onError callback on paste error', async () => {
testLogger.suppressErrors();
const onError = jest.fn();
const error = new Error('test-error');
readTextMock.mockImplementation(() => {
throw error;
});
const result = await clipboardUtils.paste({ onError });
expect(result).toEqual('');
expect(onError).toHaveBeenCalledWith(error);
});
});
});
33 changes: 33 additions & 0 deletions src/core/utils/clipboardUtils/clipboardUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface IClipboardUtilsParams {
/**
* Callback called on paste error.
*/
onError?: (error: unknown) => void;
}

class ClipboardUtils {
copy = async (value: string, params: IClipboardUtilsParams = {}): Promise<void> => {
const { onError } = params;

try {
await navigator.clipboard.writeText(value);
} catch (error: unknown) {
onError?.(error);
}
};

paste = async (params: IClipboardUtilsParams = {}): Promise<string> => {
const { onError } = params;
let clipboardText = '';

try {
clipboardText = await navigator.clipboard.readText();
} catch (error: unknown) {
onError?.(error);
}

return clipboardText;
};
}

export const clipboardUtils = new ClipboardUtils();
1 change: 1 addition & 0 deletions src/core/utils/clipboardUtils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { clipboardUtils, type IClipboardUtilsParams } from './clipboardUtils';
1 change: 1 addition & 0 deletions src/core/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './clipboardUtils';
export * from './formatterUtils';
export * from './mergeRefs';
export * from './responsiveUtils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { AddressInput, type IAddressInputProps, type IAddressInputResolvedValue } from './addressInput';

const meta: Meta<typeof AddressInput> = {
title: 'Modules/Components/Address/AddressInput',
component: AddressInput,
tags: ['autodocs'],
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/P0GeJKqILL7UXvaqu5Jj7V/v1.1.0?type=design&node-id=8192-18146&mode=design&t=VfR81DAQucRS3iGm-4',
},
},
};

type Story = StoryObj<typeof AddressInput>;

const ControlledComponent = (props: IAddressInputProps) => {
const [value, setValue] = useState<string>();
const [addressValue, setAddressValue] = useState<IAddressInputResolvedValue>();

const stringAddressValue = JSON.stringify(addressValue, null, 2) ?? 'undefined';

return (
<div className="flex grow flex-col gap-2">
<AddressInput value={value} onChange={setValue} onAccept={setAddressValue} {...props} />
<code className="[word-break:break-word]">Address value: {stringAddressValue}</code>
</div>
);
};

/**
* Default usage of the AddressInput component.
*/
export const Default: Story = {
args: {
placeholder: 'ENS or 0x …',
},
render: ({ onChange, onAccept, ...props }) => <ControlledComponent {...props} />,
};

export default meta;
Loading
Loading