diff --git a/src/components/Select/__stories__/Select.stories.tsx b/src/components/Select/__stories__/Select.stories.tsx index f7d43beef..5d2f6bccd 100644 --- a/src/components/Select/__stories__/Select.stories.tsx +++ b/src/components/Select/__stories__/Select.stories.tsx @@ -1,16 +1,36 @@ -import type {Meta, StoryObj} from '@storybook/react'; +/* eslint-disable react-hooks/rules-of-hooks */ +import * as React from 'react'; + +import {Plus, TrashBin} from '@gravity-ui/icons'; +import {useArgs} from '@storybook/preview-api'; +import type {Decorator, Meta, StoryObj} from '@storybook/react'; +import {escapeRegExp} from 'lodash'; import {Select} from '..'; import {Button} from '../../Button'; +import {Icon} from '../../Icon'; +import {Text} from '../../Text'; +import {Tooltip} from '../../Tooltip'; +import {TextInput} from '../../controls'; import {Flex} from '../../layout'; +import {block} from '../../utils/cn'; import {SelectPopupWidthShowcase} from './SelectPopupWidthShowcase'; import {SelectShowcase} from './SelectShowcase'; import {UseSelectOptionsShowcase} from './UseSelectOptionsShowcase'; +import './SelectShowcase.scss'; + +const b = block('select-showcase'); + const meta: Meta = { title: 'Components/Inputs/Select', component: Select, + argTypes: { + onUpdate: { + action: 'onUpdate', + }, + }, parameters: { a11y: { element: '#storybook-root', @@ -30,6 +50,7 @@ const meta: Meta = { export default meta; type Story = StoryObj; +type StoryArgs = Exclude; export const Default = { render: (args) => ( @@ -44,7 +65,7 @@ export const Default = { ), } satisfies Story; -export const Showcase = { +export const Showcase2 = { render: (args) => , args: { view: 'normal', @@ -92,3 +113,459 @@ export const Form = { ), } satisfies Story; + +const WithTitle: Decorator = (Story, context) => { + console.log(context); + + return ( + + + {context.name} + + + + ); +}; + +const showcaseArgs = { + ...Showcase2.args, + title: 'Sample select', +}; + +export const Simple: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithGroups: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithDisabledOptions: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithUserOptions: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + {...args} + options={undefined} + value={value} + onUpdate={(values) => setArgs({value: values})} + renderOption={(option) => { + return ( +
+ {option.content} +
+ ); + }} + renderOptionGroup={(optionGroup) => { + return
{optionGroup.label}
; + }} + getOptionHeight={() => 22} + getOptionGroupHeight={() => 32} + > + + + + + + + + + + ); + }, +}; + +export const WithUserSelectedOptions: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + {...args} + options={undefined} + value={value} + onUpdate={(values) => setArgs({value: values})} + renderOption={(option) => { + return ( +
+ {option.content} +
+ ); + }} + renderSelectedOption={(option) => { + return ( + + {option.content} + + ); + }} + getOptionHeight={() => 22} + > + + + + + + ); + }, +}; + +export const WithUserControlAndNativeCustomIcon: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: { + ...showcaseArgs, + hasClear: true, + value: ['val2'], + }, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithUserControlAndCustomPlacement: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithVirtualizedList: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithCustomRendererAndTooltipAtDisabledItem: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: showcaseArgs, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithCustomFilterSection: StoryObj< + React.ComponentProps & {matchCase: boolean; matchWholeWord: boolean} +> = { + tags: ['!dev'], + decorators: [WithTitle], + args: { + ...showcaseArgs, + filterable: true, + matchCase: false, + matchWholeWord: false, + }, + render: (args) => { + const [{value, matchCase, matchWholeWord}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithCustomPopup: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: { + ...showcaseArgs, + filterable: true, + }, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithOutsideError: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: { + ...showcaseArgs, + errorPlacement: 'outside', + errorMessage: 'A validation error has occurred', + validationState: 'invalid', + }, + render: (args) => { + const [{value}, setArgs] = useArgs(); + + return ( + + ); + }, +}; + +export const WithInsideError: Story = { + tags: ['!dev'], + decorators: [WithTitle], + args: { + ...showcaseArgs, + errorPlacement: 'inside', + errorMessage: 'A validation error has occurred', + validationState: 'invalid', + }, + render: WithOutsideError.render, +}; diff --git a/src/components/Select/__stories__/SelectShowcase.tsx b/src/components/Select/__stories__/SelectShowcase.tsx deleted file mode 100644 index a33cc1d76..000000000 --- a/src/components/Select/__stories__/SelectShowcase.tsx +++ /dev/null @@ -1,454 +0,0 @@ -import * as React from 'react'; - -import {Plus, TrashBin} from '@gravity-ui/icons'; -import range from 'lodash/range'; - -import {Select} from '..'; -import type {SelectOption, SelectProps} from '..'; -import {Button} from '../../Button'; -import {ClipboardButton} from '../../ClipboardButton'; -import {Icon} from '../../Icon'; -import {SegmentedRadioGroup} from '../../SegmentedRadioGroup'; -import {Tooltip} from '../../Tooltip'; -import {TextInput} from '../../controls'; -import {block} from '../../utils/cn'; - -import { - EXAMPLE_CHILDREN_OPTIONS, - EXAMPLE_CUSTOM_FILTER_SECTION, - EXAMPLE_CUSTOM_POPUP, - EXAMPLE_CUSTOM_RENDERER_WITH_DISABLED_ITEM, - EXAMPLE_DISABLED_OPTIONS, - EXAMPLE_GROUP_CHILDREN_OPTIONS, - EXAMPLE_GROUP_JSON_OPTIONS, - EXAMPLE_JSON_OPTIONS, - EXAMPLE_USER_CONTROL, - EXAMPLE_USER_CONTROL_WITH_PLACEMENT, - EXAMPLE_USER_OPTIONS, -} from './constants'; - -import './SelectShowcase.scss'; - -const b = block('select-showcase'); -const Mode = { - CODE: 'code', - VIEW: 'view', -}; - -const generateItems = (count: number): SelectOption[] => { - return range(0, count).map((i) => ({ - value: `val${i + 1}`, - content: `Value ${i + 1}`, - })); -}; - -const getEscapedString = (str: string) => { - return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); -}; - -const ExampleItem = (props: { - title: string; - selectProps: SelectProps; - code?: string[]; - children?: SelectProps['children']; - id?: string; -}) => { - const {title, selectProps, children, code = [], id} = props; - const multiple = props.selectProps.multiple; - const [mode, setMode] = React.useState(Mode.VIEW); - const [value, setValue] = React.useState([]); - - React.useEffect(() => { - if (!multiple) { - setValue([]); - } - }, [multiple]); - - return ( -
-

- - {Boolean(code.length) && ( - setMode(nextMode)} - > - - - - )} -

- {mode === Mode.VIEW ? ( - - ) : ( - code.map((codeItem, i) => { - return ( - -
-                                {codeItem}
-                                
-                            
-
- ); - }) - )} -
- ); -}; - -export const SelectShowcase = (props: SelectProps) => { - const [matchCase, setMatchCase] = React.useState(false); - const [matchWholeWord, setMatchWholeWord] = React.useState(false); - - const renderFilter: SelectProps['renderFilter'] = ({ - ref, - style, - inputProps: {value, onChange, onKeyDown, ...controlProps}, - }) => { - return ( -
- -
- - -
-
- ); - }; - - const getFilterOption = (): SelectProps['filterOption'] | undefined => { - if (matchCase || matchWholeWord) { - return (option, filter) => { - const flags = matchCase ? '' : 'i'; - const escapedFilter = getEscapedString(filter); - const resultFilter = matchWholeWord ? `\\b${escapedFilter}\\b` : escapedFilter; - const regExp = new RegExp(resultFilter, flags); - return regExp.test(option.content as string); - }; - } - - return undefined; - }; - - const idLocal = React.useId(); - const id = props.id || idLocal; - - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - { - return ( -
- {option.content} -
- ); - }, - renderOptionGroup: (optionGroup) => { - return ( -
{optionGroup.label}
- ); - }, - getOptionHeight: () => 22, - getOptionGroupHeight: () => 32, - }} - id={`${id}-4`} - > - - - - - - - - -
- { - return ( -
- {option.content} -
- ); - }, - renderSelectedOption: (option) => { - return ( - - {option.content} - - ); - }, - getOptionHeight: () => 22, - }} - id={`${id}-5`} - > - - - - -
- { - return ( - - ); - }, - }} - id={`${id}-6`} - > - - - - - - { - return ( - - ); - }, - }} - id={`${id}-7`} - > - - - - - - - - { - return option.disabled ? ( - - - Hover here - - - ) : ( - {option.content} - ); - }, - }} - id={`${id}-9`} - > - - - - - - - - - - { - return ( - -
{'---- Before Filter ----'}
- {renderFilter()} -
{'---- After Filter, Before List ----'}
- {renderList()} -
{'---- After List ----'}
-
- ); - }, - }} - id={`${id}-11`} - > - - - - -
- -
-

Select (with text error)

- - - - - - - - - - - - - - -
-
- ); -}; diff --git a/src/components/Select/__stories__/Showcase.mdx b/src/components/Select/__stories__/Showcase.mdx new file mode 100644 index 000000000..28be641bb --- /dev/null +++ b/src/components/Select/__stories__/Showcase.mdx @@ -0,0 +1,21 @@ +import {Canvas, Meta} from '@storybook/blocks'; +import * as Stories from './Select.stories'; + + + + + + + + + + + + + + + +## With errors + + +