Commit b2e1ae96 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Update UI toolkit (#2741)

* add more form fields and fix link props

* extend select component to render custom item and add lg size to switch

* fix build

* fix button border radius

* fix ts
parent cc08cc3c
......@@ -4,12 +4,12 @@ import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from './utils';
interface ApiPropsBase {
export interface ApiPropsBase {
endpoint: string;
basePath?: string;
}
interface ApiPropsFull extends ApiPropsBase {
export interface ApiPropsFull extends ApiPropsBase {
host: string;
protocol: string;
port?: string;
......
import { Field as ChakraField } from '@chakra-ui/react';
import * as React from 'react';
import { space } from 'toolkit/utils/htmlEntities';
import getComponentDisplayName from '../utils/getComponentDisplayName';
import { space } from '../utils/htmlEntities';
import type { InputProps } from './input';
import type { InputGroupProps } from './input-group';
......
......@@ -30,7 +30,7 @@ interface LinkPropsChakra extends ChakraLinkProps {
disabled?: boolean;
}
interface LinkPropsNext extends Pick<NextLinkProps, 'shallow' | 'prefetch' | 'scroll'> {}
interface LinkPropsNext extends Partial<Pick<NextLinkProps, 'shallow' | 'prefetch' | 'scroll'>> {}
export interface LinkProps extends LinkPropsChakra, LinkPropsNext {}
......
......@@ -13,10 +13,11 @@ import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
export interface SelectOption<Value extends string = string> {
value: Value;
label: string;
renderLabel?: () => React.ReactNode;
value: Value;
icon?: React.ReactNode;
}
};
export interface SelectControlProps extends ChakraSelect.ControlProps {
noIndicator?: boolean;
......@@ -160,7 +161,7 @@ export const SelectValueText = React.forwardRef<
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}>
{ context.collection.stringifyItem(item) }
{ item.renderLabel ? item.renderLabel() : context.collection.stringifyItem(item) }
</span>
</Flex>
</>
......@@ -257,7 +258,7 @@ export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref)
<SelectContent portalled={ portalled } { ...contentProps }>
{ collection.items.map((item: SelectOption) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
{ item.renderLabel ? item.renderLabel() : item.label }
</SelectItem>
)) }
</SelectContent>
......@@ -323,7 +324,7 @@ export const SelectAsync = React.forwardRef<HTMLDivElement, SelectAsyncProps>((p
</Box>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
{ item.renderLabel ? item.renderLabel() : item.label }
</SelectItem>
)) }
</SelectContent>
......
import { chakra, Tag as ChakraTag } from '@chakra-ui/react';
import * as React from 'react';
import { nbsp } from 'toolkit/utils/htmlEntities';
import { TruncatedTextTooltip } from '../components/truncation/TruncatedTextTooltip';
import { nbsp } from '../utils/htmlEntities';
import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
......
......@@ -9,8 +9,7 @@ import {
createToaster,
} from '@chakra-ui/react';
import { SECOND } from 'toolkit/utils/consts';
import { SECOND } from '../utils/consts';
import { CloseButton } from './close-button';
export const toaster = createToaster({
......
import type { TabItem, TabItemMenu } from './types';
import { middot } from 'toolkit/utils/htmlEntities';
import { middot } from '../../utils/htmlEntities';
export const menuButton: TabItemMenu = {
id: 'menu',
......
......@@ -6,10 +6,10 @@ import type { FormFieldPropsBase } from './types';
import { Checkbox } from '../../../chakra/checkbox';
import type { CheckboxProps } from '../../../chakra/checkbox';
interface Props<
export interface FormFieldCheckboxProps<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends Pick<FormFieldPropsBase<FormFields, Name>, 'rules' | 'name' | 'onChange' | 'readOnly'>, Omit<CheckboxProps, 'name' | 'onChange'> {
> extends Pick<FormFieldPropsBase<FormFields, Name>, 'rules' | 'name' | 'onChange' | 'readOnly' | 'controllerProps'>, Omit<CheckboxProps, 'name' | 'onChange'> {
label: string;
}
......@@ -22,13 +22,15 @@ const FormFieldCheckboxContent = <
rules,
onChange,
readOnly,
controllerProps,
...rest
}: Props<FormFields, Name>) => {
}: FormFieldCheckboxProps<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
...controllerProps,
});
const isDisabled = formState.isSubmitting;
......
import React from 'react';
import { useController, useFormContext, type FieldValues, type Path } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { CheckboxGroupProps, CheckboxProps } from '../../../chakra/checkbox';
import { Checkbox, CheckboxGroup } from '../../../chakra/checkbox';
export interface FormFieldCheckboxGroupProps<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends Pick<FormFieldPropsBase<FormFields, Name>, 'rules' | 'name' | 'onChange' | 'readOnly' | 'controllerProps'>,
Omit<CheckboxGroupProps, 'name' | 'onChange'> {
options: Array<{ label: string; value: string }>;
itemProps?: CheckboxProps;
}
const FormFieldCheckboxGroupContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: FormFieldCheckboxGroupProps<FormFields, Name>) => {
const { name, options, disabled, controllerProps, itemProps, rules, onChange, ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
...controllerProps,
});
const handleChange = React.useCallback((value: Array<string>) => {
field.onChange(value);
onChange?.();
}, [ field, onChange ]);
return (
<CheckboxGroup
ref={ field.ref }
name={ field.name }
value={ field.value }
onValueChange={ handleChange }
disabled={ formState.isSubmitting || disabled }
{ ...rest }
>
{ options.map(({ value, label }) => (
<Checkbox key={ value } value={ value } { ...itemProps }>
{ label }
</Checkbox>
)) }
</CheckboxGroup>
);
};
export const FormFieldCheckboxGroup = React.memo(FormFieldCheckboxGroupContent) as typeof FormFieldCheckboxGroupContent;
......@@ -13,7 +13,7 @@ import { InputGroup } from '../../../chakra/input-group';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
import { colorValidator } from '../validators/color';
interface Props<
export interface FormFieldColorProps<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends FormFieldPropsBase<FormFields, Name> {
......@@ -33,8 +33,9 @@ const FormFieldColorContent = <
size = 'lg',
disabled,
sampleDefaultBgColor,
controllerProps,
...restProps
}: Props<FormFields, Name>) => {
}: FormFieldColorProps<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
......@@ -45,6 +46,7 @@ const FormFieldColorContent = <
validate: colorValidator,
maxLength: 7,
},
...controllerProps,
});
const [ value, setValue ] = React.useState('');
......
import React from 'react';
import type { FieldValues } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { InputProps } from '../../../chakra/input';
import { FormFieldText } from './FormFieldText';
const FormFieldNumberContent = <FormFields extends FieldValues>(
{ inputProps, ...rest }: FormFieldPropsBase<FormFields>,
) => {
return <FormFieldText { ...rest } inputProps={{ type: 'number', ...inputProps } as InputProps}/>;
};
export const FormFieldNumber = React.memo(FormFieldNumberContent) as typeof FormFieldNumberContent;
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { ExcludeUndefined } from 'types/utils';
import type { RadioGroupProps, RadioProps } from '../../../chakra/radio';
import { Radio, RadioGroup } from '../../../chakra/radio';
export interface FormFieldRadioProps<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> extends Pick<FormFieldPropsBase<FormFields>, 'rules' | 'controllerProps'>,
RadioGroupProps {
name: Name;
options: Array<{ value: ExcludeUndefined<FormFields[Name]>; label: string }>;
itemProps?: RadioProps;
}
const FormFieldRadioContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>({
name,
options,
itemProps,
onValueChange,
disabled,
controllerProps,
...rest
}: FormFieldRadioProps<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, formState } = useController<FormFields, typeof name>({
control,
name,
...controllerProps,
});
const handleValueChange = React.useCallback(
({ value }: { value: string | null }) => {
field.onChange(value);
onValueChange?.({ value });
},
[ field, onValueChange ],
);
return (
<RadioGroup
ref={ field.ref }
name={ field.name }
value={ field.value }
onValueChange={ handleValueChange }
disabled={ formState.isSubmitting || disabled }
{ ...rest }
>
{ options.map(({ value, label }) => (
<Radio
key={ value }
value={ value }
inputProps={{ onBlur: field.onBlur }}
{ ...itemProps }
>
{ label }
</Radio>
)) }
</RadioGroup>
);
};
export const FormFieldRadio = React.memo(FormFieldRadioContent) as typeof FormFieldRadioContent;
......@@ -8,7 +8,7 @@ import type { SelectProps } from '../../../chakra/select';
import { Select } from '../../../chakra/select';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
type Props<
export type FormFieldSelectProps<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectProps;
......@@ -16,14 +16,15 @@ type Props<
const FormFieldSelectContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, size = 'lg', ...rest } = props;
>(props: FormFieldSelectProps<FormFields, Name>) => {
const { name, rules, size = 'lg', controllerProps, ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
...controllerProps,
});
const isDisabled = formState.isSubmitting;
......
......@@ -8,7 +8,7 @@ import type { SelectAsyncProps } from '../../../chakra/select';
import { SelectAsync } from '../../../chakra/select';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
type Props<
export type FormFieldSelectAsyncProps<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectAsyncProps;
......@@ -16,14 +16,15 @@ type Props<
const FormFieldSelectAsyncContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, size = 'lg', ...rest } = props;
>(props: FormFieldSelectAsyncProps<FormFields, Name>) => {
const { name, rules, size = 'lg', controllerProps, ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
...controllerProps,
});
const isDisabled = formState.isSubmitting;
......
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import { Switch } from '../../../chakra/switch';
import type { SwitchProps } from '../../../chakra/switch';
export type FormFieldSwitchProps<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = Pick<
FormFieldPropsBase<FormFields, Name>,
'name' | 'placeholder' | 'rules' | 'controllerProps'
> &
SwitchProps;
const FormFieldSwitchContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>({
name,
placeholder,
onCheckedChange,
rules,
controllerProps,
disabled,
...rest
}: FormFieldSwitchProps<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, formState } = useController<FormFields, Name>({
control,
name,
rules,
...controllerProps,
});
const handleCheckedChange = React.useCallback(({ checked }: { checked: boolean }) => {
field.onChange(checked);
onCheckedChange?.({ checked });
}, [ field, onCheckedChange ],
);
return (
<Switch
name={ field.name }
checked={ field.value }
onCheckedChange={ handleCheckedChange }
disabled={ formState.isSubmitting || disabled }
inputProps={{ onBlur: field.onBlur }}
{ ...rest }
>
{ placeholder }
</Switch>
);
};
export const FormFieldSwitch = React.memo(FormFieldSwitchContent) as typeof FormFieldSwitchContent;
......@@ -12,7 +12,7 @@ import type { TextareaProps } from '../../../chakra/textarea';
import { Textarea } from '../../../chakra/textarea';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
interface Props<
export interface FormFieldTextProps<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends FormFieldPropsBase<FormFields, Name> {
......@@ -33,8 +33,9 @@ const FormFieldTextContent = <
size: sizeProp,
disabled,
floating: floatingProp,
controllerProps,
...restProps
}: Props<FormFields, Name>) => {
}: FormFieldTextProps<FormFields, Name>) => {
const defaultSize = asComponent === 'Textarea' ? '2xl' : 'lg';
const size = sizeProp || defaultSize;
const floating = floatingProp !== undefined ? floatingProp : size === defaultSize;
......@@ -44,6 +45,7 @@ const FormFieldTextContent = <
control,
name,
rules: { ...rules, required: restProps.required },
...controllerProps,
});
const handleBlur = React.useCallback(() => {
......
......@@ -2,9 +2,14 @@ export * from './image/FormFieldImagePreview';
export * from './image/useImageField';
export * from './FormFieldAddress';
export * from './FormFieldCheckbox';
export * from './FormFieldCheckboxGroup';
export * from './FormFieldColor';
export * from './FormFieldEmail';
export * from './FormFieldNumber';
export * from './FormFieldRadio';
export * from './FormFieldSelect';
export * from './FormFieldSelectAsync';
export * from './FormFieldSwitch';
export * from './FormFieldText';
export * from './FormFieldUrl';
export * from './types';
import type React from 'react';
import type { ControllerRenderProps, FieldValues, Path, RegisterOptions } from 'react-hook-form';
import type { ControllerRenderProps, FieldPathValue, FieldValues, Path, RegisterOptions } from 'react-hook-form';
import type { FieldProps } from '../../../chakra/field';
import type { InputProps } from '../../../chakra/input';
......@@ -16,6 +16,10 @@ export interface FormFieldPropsBase<
onBlur?: () => void;
onChange?: () => void;
inputProps?: InputProps | TextareaProps;
controllerProps?: {
shouldUnregister?: boolean;
defaultValue?: FieldPathValue<FormFields, Name>;
};
group?: Omit<InputGroupProps, 'children' | 'endElement'> & {
endElement?: React.ReactNode | (({ field }: { field: ControllerRenderProps<FormFields, Name> }) => React.ReactNode);
};
......
......@@ -2,8 +2,8 @@ import { useCopyToClipboard } from '@uidotdev/usehooks';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import { SECOND } from 'toolkit/utils/consts';
import { SECOND } from '../utils/consts';
import { useDisclosure } from './useDisclosure';
// NOTE: If you don't need the disclosure and the timeout features, please use the useCopyToClipboard hook directly
......
......@@ -70,9 +70,13 @@ import { themeConfig } from '@blockscout/ui-toolkit';
const customOverrides = {
// Add your custom theme overrides here
colors: {
brand: {
primary: '#your-color',
theme: {
semanticTokens: {
colors: {
brand: {
primary: { value: '#5353D3' }
},
},
},
},
};
......
......@@ -6,6 +6,7 @@ export const recipe = defineRecipe({
gap: 0,
fontWeight: 600,
overflow: 'hidden',
borderRadius: 'base',
_disabled: {
opacity: 'control.disabled',
},
......
......@@ -18,9 +18,7 @@ export const recipe = defineSlotRecipe({
},
label: {
lineHeight: '1',
userSelect: 'none',
fontSize: 'sm',
fontWeight: '400',
_disabled: {
opacity: '0.5',
......@@ -112,6 +110,9 @@ export const recipe = defineSlotRecipe({
'--switch-height': 'sizes.4',
'--switch-indicator-font-size': 'fontSizes.sm',
},
label: {
textStyle: 'sm',
},
},
md: {
root: {
......@@ -119,6 +120,19 @@ export const recipe = defineSlotRecipe({
'--switch-height': 'sizes.5',
'--switch-indicator-font-size': 'fontSizes.md',
},
label: {
textStyle: 'sm',
},
},
lg: {
root: {
'--switch-width': '50px',
'--switch-height': 'sizes.7',
'--switch-indicator-font-size': 'fontSizes.md',
},
label: {
textStyle: 'md',
},
},
},
},
......
import { createListCollection } from '@chakra-ui/react';
import { Box, createListCollection } from '@chakra-ui/react';
import { noop } from 'es-toolkit';
import React from 'react';
......@@ -17,7 +17,7 @@ const frameworks = createListCollection<SelectOption>({
items: [
{ label: 'React.js is the most popular framework', value: 'react', icon: <IconSvg name="API" boxSize={ 5 } flexShrink={ 0 }/> },
{ label: 'Vue.js is the second most popular framework', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ value: 'angular', label: 'Angular', renderLabel: () => <div>Angular is <Box as="span" color="red" fontWeight="700">not awesome</Box></div> },
{ label: 'Svelte', value: 'svelte' },
],
});
......
......@@ -21,6 +21,11 @@ const SwitchShowcase = () => {
Show duck
</Switch>
</Sample>
<Sample label="size: lg">
<Switch size="lg">
Show duck
</Switch>
</Sample>
</SamplesStack>
</Section>
</Container>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment