diff --git a/src/components/data-entry/QueryItem/Cascader.stories.tsx b/src/components/data-entry/QueryItem/Cascader.stories.tsx index e46f39077..5d6d42dbe 100644 --- a/src/components/data-entry/QueryItem/Cascader.stories.tsx +++ b/src/components/data-entry/QueryItem/Cascader.stories.tsx @@ -1,5 +1,5 @@ import { type Meta, type StoryObj } from '@storybook/react' -import { type IQueryItemCascaderProps, QueryItem } from 'src/components' +import { Flex, Icon, type IQueryItemCascaderProps, QueryItem, Typography } from 'src/components' const options: IQueryItemCascaderProps['options'] = [ { @@ -151,3 +151,35 @@ export const Loading: Story = { }, }, } + +export const SearchLabel: Story = { + args: { + options: [ + ...options, + { + value: 'G', + label: 'G', + children: [ + { + value: 'H', + label: 'H', + children: [ + { + value: 'IJK', + label: ( + <> + + + IJK + + + ), + searchLabel: 'IJK', + }, + ], + }, + ], + }, + ], + }, +} diff --git a/src/components/data-entry/QueryItem/Cascader.tsx b/src/components/data-entry/QueryItem/Cascader.tsx index 7582fce0f..69dee0452 100644 --- a/src/components/data-entry/QueryItem/Cascader.tsx +++ b/src/components/data-entry/QueryItem/Cascader.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import './query-item.css' import { type GetProp } from 'antd' +import { type DefaultOptionType } from 'antd/es/select' import { type ReactNode, useCallback, useEffect, useState } from 'react' import { Cascader as BaseCascader, @@ -15,9 +16,10 @@ import { type Icons } from 'src/constants/Icons' import { useMount } from 'src/hooks/useMount' import { debounce } from 'src/utils/utils' -export interface ICascaderOption { +export interface ICascaderOption extends DefaultOptionType { value: string - label: string + label: ReactNode + searchLabel?: string // useful when label is a reactNode and not a string children?: ICascaderOption[] disabled?: boolean } @@ -31,18 +33,20 @@ export interface IQueryItemCascaderProps { loadData?: (value: string) => Promise value?: Array disabled?: boolean + placement?: IBaseCascaderProps['placement'] + defaultOpen?: IBaseCascaderProps['defaultOpen'] } const Cascader = (props: IQueryItemCascaderProps) => { - type DefaultOptionType = GetProp[number] + type ICascaderOption = GetProp[number] const options: ICascaderOption[] = [] const [items, setItems] = useState(props.options ?? options) const [searchValue, setSearchValue] = useState('') const [isLoading, setIsLoading] = useState(false) const [selectedValue, setSelectedValue] = useState>(props.value ?? []) - const [selectedDisplayValue, setSelectedDisplayValue] = useState( - props.value && props.value.length > 0 ? (props.value.slice(-1)[0] as any).label : '', + const [selectedOption, setSelectedOption] = useState( + props.value?.length ? props.value.slice(-1)[0] : undefined, ) const [isOpen, setIsOpen] = useState(false) @@ -79,9 +83,11 @@ const Cascader = (props: IQueryItemCascaderProps) => { setSearchValue(value) } - const filter = (inputValue: string, path: DefaultOptionType[]) => { - return path.some(option => (option.label as string).toLowerCase().includes(inputValue.toLowerCase())) - } + const getSearchValue = (option: ICascaderOption): string => + option.searchLabel ?? (typeof option.label === 'string' ? (option.label as string) : '') + + const filter = (inputValue: string, path: ICascaderOption[]) => + path.some(option => getSearchValue(option).toLowerCase().includes(inputValue.toLowerCase())) let debouncedLoadData: (value: string) => void if (props.loadData) { @@ -98,9 +104,11 @@ const Cascader = (props: IQueryItemCascaderProps) => { searchValue, disabled: props.disabled, value: selectedValue, + defaultOpen: props.defaultOpen, + placement: props.placement ?? 'bottomLeft', onChange: (values: Array, selectedOptions: any): void => { setSelectedValue(values as string[]) - setSelectedDisplayValue(selectedOptions.slice(-1)[0].label) + setSelectedOption(selectedOptions.slice(-1)[0]) void props.onChange?.(values, selectedOptions) }, dropdownRender: menu => ( @@ -111,6 +119,7 @@ const Cascader = (props: IQueryItemCascaderProps) => { <> { ), showSearch: { filter, - render: (inputValue: string, paths: ICascaderOption[]): ReactNode => { - return ( - <> - {paths.map((path: ICascaderOption, index) => ( - <> - {highlightMatches(path.label, inputValue.toLowerCase())} - {index < paths.length - 1 ? ' > ' : ''} - - ))} - - ) - }, + render: (inputValue: string, options: ICascaderOption[]): ReactNode => ( + <> + {options.map((option: ICascaderOption, index) => ( + <> + {highlightMatches(getSearchValue(option), inputValue.toLowerCase())} + {index < options.length - 1 ? ' > ' : ''} + + ))} + + ), }, options: items, onDropdownVisibleChange: value => { @@ -149,7 +156,7 @@ const Cascader = (props: IQueryItemCascaderProps) => { if (isOpen) inputClasses += ' query-item--open' if (selectedValue && selectedValue.length !== 0) inputClasses += ' query-item--selected' if (props.errorMessage) inputClasses += ' query-item--error' - const displayValue = selectedDisplayValue ?? selectedValue.slice(-1) + const displayValue = (selectedOption ? getSearchValue(selectedOption) : selectedValue.slice(-1)) as string return ( <> @@ -164,7 +171,7 @@ const Cascader = (props: IQueryItemCascaderProps) => { prefix={getIcon()} /> - {props.errorMessage && {props.errorMessage}} + {props.errorMessage && {props.errorMessage}} ) @@ -189,11 +196,8 @@ const Cascader = (props: IQueryItemCascaderProps) => { ) } - function transformOptionsToPaths( - options: DefaultOptionType[], - prefixPath: DefaultOptionType[], - ): DefaultOptionType[][] { - let result: DefaultOptionType[][] = [] + function transformOptionsToPaths(options: ICascaderOption[], prefixPath: ICascaderOption[]): ICascaderOption[][] { + let result: ICascaderOption[][] = [] options.forEach(option => { if (option.children && option.children.length > 0) { const newPrefix = prefixPath.concat([{ label: option.label, value: option.value }])