Skip to content

Commit

Permalink
fix: Apply minor fixes to form core components (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth authored Aug 20, 2024
1 parent 477506f commit 273e4e5
Show file tree
Hide file tree
Showing 37 changed files with 333 additions and 266 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Implement `useRandomId` hook and update fields components to use it
- Update `<TextAreaRichText />` core component to expose `immediatelyRender` property for SSR usage
- Update `<RadioGroup />`, `<CheckboxGroup />` and `<Switch />` core components to render the `InputContainer`
component and support the label, helpText, alert and isOptional properties.

### Fixed

- Update `<Wallet />` module component to correctly propagate custom `chainId` and `wagmi` configs to
`<MemberAvatar />` component
- Fix `FIAT_TOTAL_SHORT`, `FIAT_TOTAL_LONG`, `TOKEN_AMOUNT_SHORT` and `PERCENTAGE_SHORT` formats to truncate small
numbers
- Hide ens loader indicator on `<Wallet />` module component for mobile devices
- Fix Storybook stories path of `<Checkbox />` core components
- Move `<Radio />` core components under `/forms` folder
- Fix `<InputFileAvatar />` props interface to only expose supported props
- Fix customisation of `z-index` property on `<TextAreaRichText />` core component

### Changed

- Default `type` attribute of `<Button />` core component to `button`
- Rename `label` property on `<Switch />` core component to `inlineLabel` to also support the existing `label`
property from the `InputContainer` component.
- Update minor and patch NPM dependencies
- Bump `elliptic` from `6.5.5` to `6.5.7`

Expand Down
11 changes: 11 additions & 0 deletions src/core/components/button/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,15 @@ describe('<Button /> component', () => {
await user.click(screen.getByRole('link'));
expect(onClick).toHaveBeenCalled();
});

it('sets button type by default', () => {
render(createTestComponent());
expect(screen.getByRole<HTMLButtonElement>('button').type).toEqual('button');
});

it('allows customisation of button type', () => {
const type = 'submit';
render(createTestComponent({ type }));
expect(screen.getByRole<HTMLButtonElement>('button').type).toEqual(type);
});
});
4 changes: 2 additions & 2 deletions src/core/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,10 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, IButtonP
);
}

const buttonProps = otherProps as ButtonHTMLAttributes<HTMLButtonElement>;
const { type = 'button', ...buttonProps } = otherProps as ButtonHTMLAttributes<HTMLButtonElement>;

return (
<button disabled={isDisabled} ref={ref as Ref<HTMLButtonElement>} {...commonProps} {...buttonProps}>
<button disabled={isDisabled} ref={ref as Ref<HTMLButtonElement>} type={type} {...commonProps} {...buttonProps}>
{buttonContent}
</button>
);
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/forms/checkbox/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { Checkbox, type CheckboxState } from './checkbox';

const meta: Meta<typeof Checkbox> = {
title: 'Core/Components/Checkbox/Checkbox',
title: 'Core/Components/Forms/Checkbox',
component: Checkbox,
argTypes: {
disabled: { type: 'boolean' },
Expand Down
11 changes: 5 additions & 6 deletions src/core/components/forms/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import classNames from 'classnames';
import { forwardRef, useId, type ComponentProps } from 'react';
import { forwardRef, type ComponentProps } from 'react';
import { useRandomId } from '../../../hooks';
import { Icon, IconType } from '../../icon';

export type CheckboxState = boolean | 'indeterminate';
Expand Down Expand Up @@ -36,9 +37,7 @@ export interface ICheckboxProps extends ComponentProps<'button'> {
export const Checkbox = forwardRef<HTMLButtonElement, ICheckboxProps>((props, ref) => {
const { label, labelPosition = 'right', checked, onCheckedChange, disabled, id, className, ...otherProps } = props;

// Generate random id if id property is not set
const randomId = useId();
const processedId = id ?? randomId;
const randomId = useRandomId(id);

return (
<div
Expand All @@ -50,7 +49,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, ICheckboxProps>((props, re
)}
>
<RadixCheckbox.Root
id={processedId}
id={randomId}
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
Expand Down Expand Up @@ -83,7 +82,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, ICheckboxProps>((props, re
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
<label
htmlFor={processedId}
htmlFor={randomId}
className={classNames(
'cursor-pointer text-sm font-normal leading-tight text-neutral-500 md:text-base',
'group-hover/root:text-neutral-800',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { CheckboxCard } from './checkboxCard';

const meta: Meta<typeof CheckboxCard> = {
title: 'Core/Components/Checkbox/CheckboxCard',
title: 'Core/Components/Forms/CheckboxCard',
component: CheckboxCard,
argTypes: {
disabled: { control: 'boolean' },
Expand Down
13 changes: 6 additions & 7 deletions src/core/components/forms/checkboxCard/checkboxCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import classNames from 'classnames';
import { forwardRef, useId, type ComponentProps } from 'react';
import { forwardRef, type ComponentProps } from 'react';
import { useRandomId } from '../../../hooks';
import { Avatar } from '../../avatars';
import { Icon, IconType } from '../../icon';
import { Tag, type ITagProps } from '../../tag';
Expand Down Expand Up @@ -44,14 +45,12 @@ export interface ICheckboxCardProps extends ComponentProps<'button'> {
export const CheckboxCard = forwardRef<HTMLButtonElement, ICheckboxCardProps>((props, ref) => {
const { id, avatar, label, description, tag, className, checked, onCheckedChange, disabled, ...otherProps } = props;

// Generate random id if id property is not set
const randomId = useId();
const processedId = id ?? randomId;
const labelId = `${processedId}-label`;
const randomId = useRandomId(id);
const labelId = `${randomId}-label`;

return (
<RadixCheckbox.Root
id={processedId}
id={randomId}
ref={ref}
aria-labelledby={labelId}
checked={checked}
Expand All @@ -73,7 +72,7 @@ export const CheckboxCard = forwardRef<HTMLButtonElement, ICheckboxCardProps>((p
{avatar && <Avatar size="sm" responsiveSize={{ md: 'md' }} src={avatar} />}
<div className="flex min-w-0 flex-1 flex-col items-start gap-0.5 text-sm font-normal leading-tight md:gap-1 md:text-base">
<p
id={processedId}
id={randomId}
className={classNames(
'max-w-full cursor-pointer truncate text-neutral-800 group-data-[state=unchecked]:text-neutral-800',
'group-data-[disabled]:cursor-default group-data-[disabled]:group-data-[state=unchecked]:text-neutral-300',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CheckboxCard } from '../checkboxCard';
import { CheckboxGroup } from './checkboxGroup';

const meta: Meta<typeof CheckboxGroup> = {
title: 'Core/Components/Checkbox/CheckboxGroup',
title: 'Core/Components/Forms/CheckboxGroup',
component: CheckboxGroup,
parameters: {
design: {
Expand Down
26 changes: 22 additions & 4 deletions src/core/components/forms/checkboxGroup/checkboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import classNames from 'classnames';
import type { ComponentProps } from 'react';
import type { ReactNode } from 'react';
import { useRandomId } from '../../../hooks';
import { InputContainer, type IInputContainerBaseProps } from '../inputContainer';

export interface ICheckboxGroupProps extends ComponentProps<'div'> {}
export interface ICheckboxGroupProps
extends Pick<IInputContainerBaseProps, 'alert' | 'label' | 'helpText' | 'isOptional'> {
/**
* Additional classes for the component.
*/
className?: string;
/**
* Children of the component.
*/
children?: ReactNode;
}

export const CheckboxGroup: React.FC<ICheckboxGroupProps> = (props) => {
const { className, ...otherProps } = props;
const { className, children, ...otherProps } = props;

return <div className={classNames('flex min-w-0 flex-col gap-2 md:gap-3', className)} {...otherProps} />;
const id = useRandomId();

return (
<InputContainer id={id} useCustomWrapper={true} {...otherProps}>
<div className={classNames('flex min-w-0 flex-col gap-2 md:gap-3', className)}>{children}</div>
</InputContainer>
);
};
12 changes: 5 additions & 7 deletions src/core/components/forms/hooks/useInputProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import classNames from 'classnames';
import { useEffect, useId, useState, type ChangeEvent, type InputHTMLAttributes } from 'react';
import { useEffect, useState, type ChangeEvent, type InputHTMLAttributes } from 'react';
import { useRandomId } from '../../../hooks';
import type { IInputComponentProps, IInputContainerProps, InputComponentElement } from '../inputContainer';

export interface IUseInputPropsResult<TElement extends InputComponentElement> {
Expand Down Expand Up @@ -38,10 +39,7 @@ export const useInputProps = <TElement extends InputComponentElement>(
...inputElementProps
} = props;

// Set a random generated id to the input field when id property is not defined to properly link the
// input with the label
const randomId = useId();
const processedId = id ?? randomId;
const randomId = useRandomId(id);

const [inputLength, setInputLength] = useState(0);

Expand All @@ -57,7 +55,7 @@ export const useInputProps = <TElement extends InputComponentElement>(
isOptional,
alert,
disabled,
id: processedId,
id: randomId,
maxLength,
className,
inputLength,
Expand All @@ -71,7 +69,7 @@ export const useInputProps = <TElement extends InputComponentElement>(
);

const inputProps = {
id: processedId,
id: randomId,
disabled: disabled,
className: inputClasses,
onChange: handleOnChange,
Expand Down
2 changes: 2 additions & 0 deletions src/core/components/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from './inputNumberMax';
export * from './inputSearch';
export * from './inputText';
export * from './inputTime';
export * from './radio';
export * from './radioCard';
export * from './radioGroup';
export * from './switch';
export * from './textArea';
Expand Down
17 changes: 6 additions & 11 deletions src/core/components/forms/inputFileAvatar/inputFileAvatar.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Accept } from 'react-dropzone';
import { type IInputComponentProps } from '../inputContainer';
import { type IInputContainerBaseProps } from '../inputContainer';

export enum InputFileAvatarError {
SQUARE_ONLY = 'square-only',
Expand All @@ -11,16 +11,7 @@ export enum InputFileAvatarError {
}

export interface IInputFileAvatarProps
extends Omit<
IInputComponentProps,
| 'maxLength'
| 'onChange'
| 'inputLength'
| 'wrapperClassName'
| 'useCustomWrapper'
| 'inputClassName'
| 'multiple'
> {
extends Pick<IInputContainerBaseProps, 'alert' | 'label' | 'helpText' | 'isOptional' | 'variant' | 'disabled'> {
/**
* Function that is called when a file is selected. Passes the file to the parent component.
* If the file is rejected, the function is not called.
Expand Down Expand Up @@ -53,4 +44,8 @@ export interface IInputFileAvatarProps
* If true, only square images are accepted.
*/
onlySquare?: boolean;
/**
* Optional ID for the file avatar input.
*/
id?: string;
}
38 changes: 20 additions & 18 deletions src/core/components/forms/inputFileAvatar/inputFileAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import classNames from 'classnames';
import { useCallback, useState } from 'react';
import { ErrorCode, useDropzone, type FileRejection } from 'react-dropzone';
import { useRandomId } from '../../../hooks';
import { Avatar } from '../../avatars';
import { Icon, IconType } from '../../icon';
import { Spinner } from '../../spinner';
import { useInputProps } from '../hooks';
import { InputContainer, type InputVariant } from '../inputContainer';
import { InputFileAvatarError, type IInputFileAvatarProps } from './inputFileAvatar.api';

Expand Down Expand Up @@ -39,24 +39,26 @@ const dropzoneErrorToError: Record<string, InputFileAvatarError | undefined> = {
[ErrorCode.TooManyFiles]: InputFileAvatarError.TOO_MANY_FILES,
};

export const InputFileAvatar: React.FC<IInputFileAvatarProps> = ({
onFileSelect,
onFileError,
maxFileSize,
minDimension,
maxDimension,
acceptedFileTypes = { 'image/png': [], 'image/gif': [], 'image/jpeg': ['.jpg', '.jpeg'] },
onlySquare,
variant = 'default',
disabled,
...otherProps
}) => {
export const InputFileAvatar: React.FC<IInputFileAvatarProps> = (props) => {
const {
onFileSelect,
onFileError,
maxFileSize,
minDimension,
maxDimension,
acceptedFileTypes = { 'image/png': [], 'image/gif': [], 'image/jpeg': ['.jpg', '.jpeg'] },
onlySquare,
variant = 'default',
disabled,
...otherProps
} = props;

const { id, ...containerProps } = otherProps;
const randomId = useRandomId(id);

const [imagePreview, setImagePreview] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

const { containerProps } = useInputProps(otherProps);
const { id, ...otherContainerProps } = containerProps;

const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (rejectedFiles.length > 0) {
Expand Down Expand Up @@ -124,9 +126,9 @@ export const InputFileAvatar: React.FC<IInputFileAvatarProps> = ({
);

return (
<InputContainer id={id} useCustomWrapper={true} {...otherContainerProps}>
<InputContainer id={randomId} useCustomWrapper={true} {...containerProps}>
<div {...getRootProps()} className={inputAvatarClassNames}>
<input {...getInputProps()} id={id} />
<input {...getInputProps()} id={randomId} />
{imagePreview ? (
<div className="relative">
<Avatar src={imagePreview} size="lg" className="cursor-pointer" data-testid="avatar" />
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { RadioGroup } from '..';
import { IconType } from '../../../icon';
import { IconType } from '../../icon';
import { Radio, type IRadioProps } from './radio';

describe('<Radio/> component', () => {
Expand Down
Loading

0 comments on commit 273e4e5

Please sign in to comment.