Commit fb77fae0 authored by tom's avatar tom

lg size for select

parent 9800d2d4
'use client';
import type { CollectionItem, ListCollection } from '@chakra-ui/react';
import { Select as ChakraSelect, createListCollection, Portal, useSelectContext } from '@chakra-ui/react';
import { Box, Select as ChakraSelect, createListCollection, Flex, Portal, useSelectContext } from '@chakra-ui/react';
import { useDebounce } from '@uidotdev/usehooks';
import * as React from 'react';
......@@ -111,54 +111,78 @@ export const SelectItem = React.forwardRef<
);
});
interface SelectValueTextProps
extends Omit<ChakraSelect.ValueTextProps, 'children'> {
interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
children?(items: Array<CollectionItem>): React.ReactNode;
size?: SelectRootProps['size'];
required?: boolean;
invalid?: boolean;
errorText?: string;
}
export const SelectValueText = React.forwardRef<
HTMLSpanElement,
SelectValueTextProps
>(function SelectValueText(props, ref) {
const { children, ...rest } = props;
const { children, size, required, invalid, errorText, ...rest } = props;
const context = useSelectContext();
const content = (() => {
const items = context.selectedItems;
const placeholder = `${ props.placeholder }${ required ? '*' : '' }${ invalid && errorText ? ` - ${ errorText }` : '' }`;
if (items.length === 0) return placeholder;
if (children) return children(items);
if (items.length === 1) {
const item = items[0] as CollectionItem & { icon?: React.ReactNode };
const icon = (() => {
if (item.icon) {
return typeof item.icon === 'string' ? <IconSvg name={ item.icon } boxSize={ 5 } flexShrink={ 0 } mr={ 1 }/> : item.icon;
}
return null;
})();
const label = size === 'lg' ? (
<Box
textStyle="xs"
color={ invalid ? 'field.placeholder.error' : 'field.placeholder' }
display="-webkit-box"
>
{ placeholder }
</Box>
) : null;
return (
<>
{ label }
<Flex display="inline-flex" alignItems="center" flexWrap="nowrap">
{ icon }
<span style={{
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}>
{ context.collection.stringifyItem(item) }
</span>
</Flex>
</>
);
}
// FIXME: we don't have multiple selection in the select yet
return `${ items.length } selected`;
})();
return (
<ChakraSelect.ValueText { ...rest } display="inline-flex" alignItems="center" flexWrap="nowrap" ref={ ref }>
<ChakraSelect.Context>
{ (select) => {
const items = select.selectedItems;
if (items.length === 0) return props.placeholder;
if (children) return children(items);
if (items.length === 1) {
const item = items[0] as CollectionItem & { icon?: React.ReactNode };
const icon = (() => {
if (item.icon) {
return typeof item.icon === 'string' ? <IconSvg name={ item.icon } boxSize={ 5 } flexShrink={ 0 } mr={ 1 }/> : item.icon;
}
return null;
})();
return (
<>
{ icon }
<span style={{
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}>
{ select.collection.stringifyItem(item) }
</span>
</>
);
}
// FIXME: we don't have multiple selection in the select yet
return `${ items.length } selected`;
} }
</ChakraSelect.Context>
<ChakraSelect.ValueText
ref={ ref }
{ ...rest }
>
{ content }
</ChakraSelect.ValueText>
);
});
......@@ -169,11 +193,14 @@ export const SelectRoot = React.forwardRef<
HTMLDivElement,
ChakraSelect.RootProps
>(function SelectRoot(props, ref) {
const { lazyMount = true, unmountOnExit = true, ...rest } = props;
return (
<ChakraSelect.Root
{ ...props }
ref={ ref }
positioning={{ sameWidth: false, ...props.positioning, offset: { mainAxis: 4, ...props.positioning?.offset } }}
lazyMount={ lazyMount }
unmountOnExit={ unmountOnExit }
{ ...rest }
positioning={{ sameWidth: true, ...props.positioning, offset: { mainAxis: 4, ...props.positioning?.offset } }}
>
{ props.asChild ? (
props.children
......@@ -212,19 +239,25 @@ export interface SelectProps extends SelectRootProps {
placeholder: string;
portalled?: boolean;
loading?: boolean;
errorText?: string;
}
export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
const { collection, placeholder, portalled = true, loading, ...rest } = props;
const { collection, placeholder, portalled = true, loading, errorText, ...rest } = props;
return (
<SelectRoot
ref={ ref }
collection={ collection }
variant="outline"
{ ...rest }
>
<SelectControl loading={ loading }>
<SelectValueText placeholder={ placeholder }/>
<SelectValueText
placeholder={ placeholder }
size={ props.size }
required={ props.required }
invalid={ props.invalid }
errorText={ errorText }
/>
</SelectControl>
<SelectContent portalled={ portalled }>
{ collection.items.map((item) => (
......@@ -237,7 +270,7 @@ export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref)
);
});
export interface SelectAsyncProps extends Omit<SelectRootProps, 'collection'> {
export interface SelectAsyncProps extends Omit<SelectProps, 'collection'> {
placeholder: string;
portalled?: boolean;
loading?: boolean;
......@@ -246,7 +279,7 @@ export interface SelectAsyncProps extends Omit<SelectRootProps, 'collection'> {
}
export const SelectAsync = React.forwardRef<HTMLDivElement, SelectAsyncProps>((props, ref) => {
const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, ...rest } = props;
const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, errorText, ...rest } = props;
const [ collection, setCollection ] = React.useState<ListCollection<CollectionItem>>(createListCollection({ items: [] }));
const [ inputValue, setInputValue ] = React.useState('');
......@@ -271,20 +304,28 @@ export const SelectAsync = React.forwardRef<HTMLDivElement, SelectAsyncProps>((p
<SelectRoot
ref={ ref }
collection={ collection }
variant="outline"
onValueChange={ handleValueChange }
{ ...rest }
>
<SelectControl loading={ loading }>
<SelectValueText placeholder={ placeholder }/>
<SelectValueText
placeholder={ placeholder }
size={ props.size }
required={ props.required }
invalid={ props.invalid }
errorText={ errorText }
/>
</SelectControl>
<SelectContent portalled={ portalled }>
<FilterInput
placeholder="Search"
initialValue={ inputValue }
onChange={ handleFilterChange }
/>
{ extraControls }
<Box px="4">
<FilterInput
placeholder="Search"
initialValue={ inputValue }
onChange={ handleFilterChange }
inputProps={{ pl: '9' }}
/>
{ extraControls }
</Box>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
......
......@@ -289,7 +289,6 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
trigger: {
outline: {
fg: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
border: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.600}' } },
},
},
item: {
......@@ -297,6 +296,17 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
highlighted: { value: { _light: '{colors.blue.50}', _dark: '{colors.whiteAlpha.100}' } },
},
},
indicator: {
fg: {
DEFAULT: { value: '{colors.gray.500}' },
},
},
placeholder: {
fg: {
DEFAULT: { value: '{colors.gray.500}' },
error: { value: '{colors.red.500}' },
},
},
},
menu: {
item: {
......
......@@ -7,7 +7,7 @@ export const recipe = defineSlotRecipe({
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'fit-content',
width: '100%',
},
trigger: {
display: 'flex',
......@@ -26,6 +26,11 @@ export const recipe = defineSlotRecipe({
_disabled: {
opacity: 'control.disabled',
},
_placeholderShown: {
'& [data-part=value-text]': {
display: '-webkit-box',
},
},
},
indicatorGroup: {
display: 'flex',
......@@ -37,14 +42,12 @@ export const recipe = defineSlotRecipe({
bottom: '0',
px: '0',
pointerEvents: 'none',
_peerHover: {
color: 'link.primary.hover',
},
},
indicator: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSize: '5',
color: 'inherit',
_open: {
color: 'link.primary.hover',
......@@ -62,7 +65,6 @@ export const recipe = defineSlotRecipe({
boxShadowColor: 'colors.popover.shadow',
maxH: '96',
overflowY: 'auto',
width: 'max-content',
minWidth: '150px',
rowGap: '2',
_open: {
......@@ -115,8 +117,11 @@ export const recipe = defineSlotRecipe({
},
},
valueText: {
display: 'flex',
flexDirection: 'column',
lineClamp: '1',
maxW: '100%',
wordBreak: 'break-all',
},
},
......@@ -127,20 +132,67 @@ export const recipe = defineSlotRecipe({
borderWidth: '2px',
color: 'select.trigger.outline.fg',
bgColor: 'transparent',
borderColor: 'select.trigger.outline.border',
borderColor: 'input.border.filled',
_expanded: {
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
_hover: {
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
},
_hover: {
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
color: 'select.trigger.outline.fg',
borderColor: 'input.border.hover',
},
_focusVisible: {
borderColor: 'link.primary.hover',
borderColor: 'input.border.focus',
},
_readOnly: {
userSelect: 'all',
pointerEvents: 'none',
cursor: 'default',
bg: 'input.bg.readOnly',
borderColor: 'input.border.readOnly',
_focus: {
borderColor: 'input.border.readOnly',
},
_hover: {
borderColor: 'input.border.readOnly',
},
},
_invalid: {
borderColor: 'border.error',
borderColor: 'input.border.error',
_hover: {
borderColor: 'input.border.error',
},
_expanded: {
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
_hover: {
color: 'link.primary.hover',
borderColor: 'link.primary.hover',
},
},
},
_placeholderShown: {
color: 'select.placeholder.fg',
borderColor: 'input.border',
_hover: {
color: 'select.placeholder.fg',
},
_invalid: {
color: 'select.placeholder.fg.error',
_hover: {
color: 'select.placeholder.fg.error',
},
},
},
},
indicatorGroup: {
color: 'select.indicator.fg',
_peerDisabled: {
opacity: 'control.disabled',
},
},
},
......@@ -162,10 +214,6 @@ export const recipe = defineSlotRecipe({
textStyle: 'sm',
gap: '1',
},
indicator: {
width: '5',
height: '5',
},
indicatorGroup: {
pr: '2',
pl: '1',
......@@ -174,17 +222,47 @@ export const recipe = defineSlotRecipe({
py: '5px',
px: '4',
},
itemGroup: {
mt: '1',
},
lg: {
root: {
'--select-trigger-height': '60px',
'--select-trigger-padding-right': '44px',
'--select-trigger-padding-left': 'spacing.4',
},
itemGroupLabel: {
py: '1',
px: '1.5',
content: {
px: '0',
py: '4',
textStyle: 'md',
},
trigger: {
py: '2',
},
item: {
py: '5px',
px: '4',
},
indicatorGroup: {
pr: '4',
pl: '2',
},
},
},
},
compoundVariants: [
{
size: 'sm',
variant: 'outline',
css: {
trigger: {
_placeholderShown: {
color: 'select.trigger.outline.fg',
},
},
},
},
],
defaultVariants: {
size: 'sm',
variant: 'outline',
......
......@@ -67,10 +67,11 @@ const ContractVerificationFieldCompiler = ({ isVyper, isStylus }: Props) => {
const extraControls = !isVyper && !isStylus ? (
<Checkbox
mb={ 2 }
mt={ 2 }
checked={ isNightly }
onCheckedChange={ handleCheckboxChange }
disabled={ formState.isSubmitting }
size="sm"
>
Include nightly builds
</Checkbox>
......
......@@ -26,6 +26,7 @@ const PopoverFilterRadio = ({ name, hasActiveFilter, collection, isLoading, onCh
collection={ collection }
defaultValue={ initialValue ? [ initialValue ] : [ collection.items[0].value ] }
onValueChange={ handleValueChange }
positioning={{ sameWidth: false }}
>
<SelectControl
triggerProps={{ asChild: true, px: { base: 1, lg: 2 } }}
......
......@@ -4,19 +4,21 @@ import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { SelectRootProps } from 'toolkit/chakra/select';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import type { SelectProps } from 'toolkit/chakra/select';
import { Select } from 'toolkit/chakra/select';
import getFieldErrorText from '../utils/getFieldErrorText';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectRootProps;
> = FormFieldPropsBase<FormFields, Name> & SelectProps;
const FormFieldSelect = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, collection, placeholder, ...rest } = props;
const { name, rules, size = 'lg', ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
......@@ -36,28 +38,19 @@ const FormFieldSelect = <
}, [ field ]);
return (
<SelectRoot
<Select
ref={ field.ref }
name={ field.name }
value={ field.value }
onBlur={ field.onBlur }
onValueChange={ handleChange }
onInteractOutside={ handleBlur }
collection={ collection }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
errorText={ getFieldErrorText(fieldState.error) }
size={ size }
{ ...rest }
>
<SelectControl>
<SelectValueText placeholder={ placeholder }/>
</SelectControl>
<SelectContent>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
/>
);
};
......
......@@ -7,6 +7,8 @@ import type { FormFieldPropsBase } from './types';
import type { SelectAsyncProps } from 'toolkit/chakra/select';
import { SelectAsync } from 'toolkit/chakra/select';
import getFieldErrorText from '../utils/getFieldErrorText';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
......@@ -16,7 +18,7 @@ const FormFieldSelectAsync = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, ...rest } = props;
const { name, rules, size = 'lg', ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
......@@ -40,10 +42,13 @@ const FormFieldSelectAsync = <
ref={ field.ref }
name={ field.name }
value={ field.value }
onBlur={ field.onBlur }
onValueChange={ handleChange }
onInteractOutside={ handleBlur }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
errorText={ getFieldErrorText(fieldState.error) }
size={ size }
{ ...rest }
/>
);
......
......@@ -55,7 +55,7 @@ const Sort = (props: Props) => {
})();
return (
<SelectRoot collection={ collection } { ...rest }>
<SelectRoot collection={ collection } positioning={{ sameWidth: false }} { ...rest }>
{ trigger }
<SelectContent>
{ collection.items.map((item) => (
......
......@@ -2,8 +2,9 @@ import { createListCollection } from '@chakra-ui/react';
import { noop } from 'es-toolkit';
import React from 'react';
import { Checkbox } from 'toolkit/chakra/checkbox';
import { NativeSelectField, NativeSelectRoot } from 'toolkit/chakra/native-select';
import { SelectContent, SelectItem, SelectRoot, SelectControl, SelectValueText } from 'toolkit/chakra/select';
import { Select, SelectAsync } from 'toolkit/chakra/select';
import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio';
import Sort from 'ui/shared/sort/Sort';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
......@@ -36,18 +37,91 @@ const SelectShowcase = () => {
<SectionHeader>Variant</SectionHeader>
<SamplesStack>
<Sample label="variant: outline">
<SelectRoot collection={ frameworks } variant="outline" defaultValue={ [ frameworks.items[0].value ] }>
<SelectControl w="200px">
<SelectValueText placeholder="Select framework"/>
</SelectControl>
<SelectContent>
{ frameworks.items.map((framework) => (
<SelectItem item={ framework } key={ framework.value }>
{ framework.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
<Select
collection={ frameworks }
placeholder="Select framework"
size="sm"
w="200px"
/>
</Sample>
</SamplesStack>
</Section>
<Section>
<SectionHeader>Size</SectionHeader>
<SamplesStack>
{ [ 'sm' as const, 'lg' as const ].map((size) => {
return (
<Sample label={ `size: ${ size }` } key={ size }>
<Select
collection={ frameworks }
placeholder="Select framework"
size={ size }
w="300px"
/>
<Select
collection={ frameworks }
placeholder="Select framework"
defaultValue={ [ frameworks.items[0].value ] }
size={ size }
w="300px"
/>
<Select
collection={ frameworks }
placeholder="Select framework"
defaultValue={ [ frameworks.items[0].value ] }
size={ size }
w="300px"
readOnly
/>
<Select
collection={ frameworks }
placeholder="Select framework"
defaultValue={ [ frameworks.items[0].value ] }
size={ size }
w="300px"
disabled
/>
<Select
collection={ frameworks }
placeholder="Select framework"
size={ size }
w="300px"
required
invalid
errorText="Error message"
/>
<Select
collection={ frameworks }
placeholder="Select framework"
defaultValue={ [ frameworks.items[0].value ] }
size={ size }
w="300px"
required
invalid
errorText="Error message"
/>
</Sample>
);
}) }
</SamplesStack>
</Section>
<Section>
<SectionHeader>With search (async)</SectionHeader>
<SamplesStack>
<Sample label="variant: outline">
<SelectAsync
placeholder="Select framework"
size="lg"
w="300px"
// eslint-disable-next-line react/jsx-no-bind
loadOptions={ () => {
return Promise.resolve(frameworks);
} }
extraControls={ <Checkbox mt={ 2 } size="sm">Include nightly builds</Checkbox> }
/>
</Sample>
</SamplesStack>
</Section>
......
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