Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
"moment": "^2.30.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^6.1.11"
"styled-components": "^6.1.11",
"rc-select": "^14.10.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json,css,md}": [
Expand Down
197 changes: 99 additions & 98 deletions src/lib/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Icon, Tooltip } from '@components';
import { withDataId } from '@components/DataId/withDataId';
import { SelectOptionStyle, StyledSelectDropdown, StyledSpanOption, StyledSpanOptionSelected } from '@styles/Select/StyledSelect';
import { colors } from 'index';
import { FlattenOptionData } from 'rc-select/lib/interface';
import { BaseOptionType } from 'rc-select/lib/Select';
import { filterOption, findSubstringIndices, getOptionsBySearch, getRegExpBasedOnInput } from './selectUtils';
import { ButtonPaginationSelector } from './ButtonPaginationSelector';

Expand All @@ -22,7 +24,7 @@ type CustomTagProps = {
closable: boolean;
};

type Option = {
interface Option extends BaseOptionType {
value: string | number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also extend from types by using type Option = BaseOptionType & {whathever}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every day learning something new :)

label: string;
color?: string;
Expand Down Expand Up @@ -136,71 +138,80 @@ const isDisabledOption = (option: Option, selectedValues: Array<string | number>
return option.disabled;
};

export const singleOptionsRenderer = (options: Option[], selectedValue: string | number | undefined, theme: Theme, dataId: string) => <>
{options.map((option) => (
<AntdSelect.Option
id={option.value}
className='option-select'
key={option.value}
disabled={option.disabled}
value={option.value}
type RendererFunction = (option: FlattenOptionData<Option>, info: { index: number }) => React.ReactNode;

const baseRenderFunction: RendererFunction = (option) => <span>{option.label}</span>;

const singleSelectRenderOptionGenerator = (selectedValues: Array<string | number>, theme: Theme, renderOptionFn?: RendererFunction): RendererFunction => {
const render = renderOptionFn || baseRenderFunction;
return (option: FlattenOptionData<Option>, info) => {
const { data } = option;
if (data.value && selectedValues.includes(data.value)) {
return (
<StyledSpanOptionSelected
theme={theme}
selected={selectedValue === option.value}
data-testid={`select-option-${option.value}`}
data-id={`${dataId}.select-option-${option.value}`}
aria-label={option.label}
$isSingleSelect
data-testid={`option-span-${data.value}`}
data-id={`select.option-span-${data.value}`}
aria-label={String(data.label || '')}
data-label={data.label}
value={String(data.value)}
>
{selectedValue === option.value ? (
<StyledSpanOptionSelected theme={theme} $isSingleSelect>
{option.label}
</StyledSpanOptionSelected>
) : (
<StyledSpanOption data-testid={`option-span-${option.value}`} data-id={`${dataId}.option-span-${option.value}`} value={option.label}>
{option.label}
</StyledSpanOption>
)}
</AntdSelect.Option>
))}
</>
{render(option, info)}
</StyledSpanOptionSelected>
);
}
return (
<StyledSpanOption
data-testid={`option-span-${data.value}`}
data-id={`select.option-span-${data.value}`}
value={String(data.value)}
theme={theme}
aria-label={String(data.label || '')}
data-label={data.label}
data-value={String(data.value)}
>
{render(option, info)}
</StyledSpanOption>
);
};
};

export const optionsRenderer = (options: Option[], selectedValues: Array<string | number>, searchValue: string, theme: Theme, dataId: string, currentPage: number, pageSize?: number) => {
const startIndex = (currentPage - 1) * (pageSize ?? options.length);
const endIndex = startIndex + (pageSize ?? options.length);
let optionsToRender = searchValue === '' ? options : (getOptionsBySearch(options, searchValue) as Option[]);
optionsToRender = optionsToRender.slice(startIndex, endIndex);
return (
<>
{optionsToRender.map((option) => {
const backgroundColor = selectedValues.includes(option.value) ? (option.color ? get(theme.color, option.color) : colors.gray400) : colors.white;
return (
<AntdSelect.Option
id={option.value}
className='option-select'
key={option.value}
disabled={isDisabledOption(option, selectedValues, pageSize)}
value={option.value}
theme={theme}
color={option.color}
style={{
backgroundColor,
}}
selected={selectedValues.includes(option.value)}
data-testid={`select-option-${option.value}`}
data-id={`${dataId}.select-option-${option.value}`}
aria-label={option.label}
>
{selectedValues.includes(option.value) ? (
<StyledSpanOptionSelected theme={theme} color={option.color}>
{option.label}
</StyledSpanOptionSelected>
) : (
renderUnselectedOption(option.label, searchValue, dataId)
)}
</AntdSelect.Option>
);
})}
</>
);
const multipleSelectRenderOptionGenerator = (selectedValues: Array<string | number>, theme: Theme, renderOptionFn?: RendererFunction): RendererFunction => {
const render = renderOptionFn || baseRenderFunction;

return (option, info) => {
const { data } = option;
const backgroundColor = selectedValues.includes(data.value) ? get(theme.color, data.color || '') || colors.gray400 : colors.white;
if (data.value && selectedValues.includes(data.value)) {
return (
<StyledSpanOptionSelected
theme={theme}
data-testid={`option-span-${data.value}`}
data-id={`select.option-span-${data.value}`}
aria-label={String(data.label || '')}
data-label={data.label}
value={String(option?.value)}
color={backgroundColor}
>
{render(option, info)}
</StyledSpanOptionSelected>
);
}
return (
<StyledSpanOption
data-testid={`option-span-${data.value}`}
data-id={`select.option-span-${data.value}`}
value={String(data.value)}
theme={theme}
aria-label={String(data.label || '')}
data-label={data.label}
data-value={String(data.value)}
>
{render(option, info)}
</StyledSpanOption>
);
};
};

export type SelectTextProps = {
Expand Down Expand Up @@ -271,14 +282,15 @@ export const Select = withDataId(
const ref = useRef<BaseSelectRef | null>(null);
const sValue = useRef('');
const th = useContext(ThemeContext) || defaultTheme;
const options = originalOptions || [];

const options = (originalOptions || []).map((option) => ({
disabled: isDisabledOption(option, selectedValues, pageSize),
...option}));
useEffect(() => {
setCurrentPage(1);
}, [searchValue]);

useEffect(() => {
if (defaultValues ) {
if (defaultValues) {
setSelectedValues(defaultValues);
}
}, [defaultValues]);
Expand Down Expand Up @@ -315,6 +327,11 @@ export const Select = withDataId(
setShowDropdown(false);
};

const optionRender =
mode === 'multiple'
? multipleSelectRenderOptionGenerator(selectedValues, th, props.optionRender)
: singleSelectRenderOptionGenerator(selectedValues, th, props.optionRender);

return (
<>
<SelectOptionStyle $theme={th} />
Expand All @@ -325,16 +342,10 @@ export const Select = withDataId(
autoClearSearchValue
data-id={dataId}
defaultValue={defaultValues}
filterOption={(input: string, option?: any) => {
const opt = options.find(x => x.value === option.value)
if (opt && opt?.label) {
return (opt.label as string).toLowerCase().includes(input.toLowerCase());
}
return false;
}}
optionRender={optionRender as (option: FlattenOptionData<BaseOptionType>, info: { index: number }) => React.ReactNode}
options={options}
loading={isLoading}
placeholder={placeholder}
open={showDropdown}
ref={(r) => {
ref.current = r;
}}
Expand Down Expand Up @@ -364,9 +375,9 @@ export const Select = withDataId(
if (showDropdown) {
closeDropdown();
e.stopPropagation();
}
else
} else {
setShowDropdown(true);
}
}}
ariaLabel={showDropdown ? hideOptionsAriaLabel : showOptionsAriaLabel}
/>
Expand Down Expand Up @@ -395,33 +406,21 @@ export const Select = withDataId(
aria-disabled={disabled}
aria-expanded={showDropdown}
{...props}
>
{singleOptionsRenderer(options, selectedValues.length > 0 ? selectedValues[0] : undefined, th, dataId)}
</AntdSelect >
/>
) : (
<AntdSelect
options={options}
autoClearSearchValue={false}
data-id={dataId}
data-testid='select'
defaultValue={defaultValues}
dropdownRender={
text
? (menu: ReactElement) =>
dropdownRenderSelect(
menu,
currentPage,
options,
handleChangePage,
handleSelectAll,
text,
searchValue,
mode,
th,
pageSize
)
? (menu: ReactElement) => dropdownRenderSelect(menu, currentPage, options, handleChangePage, handleSelectAll, text, searchValue, mode, th, pageSize)
: undefined
}
optionFilterProp='children'
optionRender={optionRender as (option: FlattenOptionData<BaseOptionType>, info: { index: number }) => React.ReactNode}
filterOption={filterOption}
maxTagCount='responsive'
maxTagPlaceholder={(displayValue: DisplayValue[]) => {
Expand Down Expand Up @@ -459,9 +458,9 @@ export const Select = withDataId(
if (showDropdown) {
closeDropdown();
e.stopPropagation();
}
else
} else {
setShowDropdown(true);
}
}}
ariaLabel={showDropdown ? hideOptionsAriaLabel : showOptionsAriaLabel}
/>
Expand All @@ -480,7 +479,11 @@ export const Select = withDataId(
}
}
}}
tagRender={maxTagLength ? (customTagProps: CustomTagProps) => tagRenderButtonPagination(customTagProps, options, maxTagLength, th, deleteOptionSelectedAriaLabel || '') : undefined}
tagRender={
maxTagLength
? (customTagProps: CustomTagProps) => tagRenderButtonPagination(customTagProps, options, maxTagLength, th, deleteOptionSelectedAriaLabel || '')
: undefined
}
value={selectedValues}
dropdownAlign={{ offset: [0, 3] }}
onChange={(values, _options) => {
Expand All @@ -505,9 +508,7 @@ export const Select = withDataId(
aria-disabled={disabled}
aria-expanded={showDropdown}
{...props}
>
{optionsRenderer(options, selectedValues, searchValue, th, dataId, currentPage, pageSize)}
</AntdSelect>
/>
)}
</>
);
Expand Down
28 changes: 17 additions & 11 deletions src/lib/styles/Select/StyledSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,26 @@ export const StyledPaginationSelector = styled.div`
export const StyledSelectDropdown = styled.div`
.ant-select-item {
min-height: auto;
background-color: transparent;
}
.ant-select-item-option-state {
display: flex;
position: relative;
left: -20px;
align-items: center;
padding-right: 8px;
}

.ant-select-item-option {
right: -26px;
}
`;

const getSpanColor = (theme: Theme, _color: string) => get(theme.color, _color);
const getSpanColor = (theme: Theme, color: string) => theme.color[color] || color;

export const StyledSpanOptionSelected = styled.span<{ theme: Theme; icon?: any; closable?: any; color?: string, $isSingleSelect?: boolean }>`
export const StyledSpanOptionSelected = styled.span<{ value?: string; theme: Theme; icon?: any; closable?: any; color?: string; $isSingleSelect?: boolean }>`
display: flex;
align-items: center;
padding: 2px 4px;
padding: 2px 0px 2px 4px;
font-weight: 400;
white-space: nowrap;
cursor: pointer;
Expand All @@ -96,20 +102,20 @@ export const StyledSpanOptionSelected = styled.span<{ theme: Theme; icon?: any;
margin: 0px;
font-size: 14px;
line-height: 14px;
background: ${(props) => !props.$isSingleSelect && props.color ? getSpanColor(props.theme, props.color) : (!props.$isSingleSelect && gray400(props.theme))};
color: ${(props) => !props.$isSingleSelect ? white(props.theme) : false};
background: ${(props) => (!props.$isSingleSelect && props.color ? getSpanColor(props.theme, props.color) : !props.$isSingleSelect && gray400(props.theme))};
color: ${(props) => (!props.$isSingleSelect ? white(props.theme) : false)};
${StyledIcon} {
${(props) =>
props.icon &&
css`
props.icon &&
css`
&.icon {
margin-right: 3px;
margin-left: -2px;
}
`};
${(props) =>
props.closable &&
css`
props.closable &&
css`
&.icon-close {
cursor: pointer;
margin-right: -2px;
Expand All @@ -120,7 +126,7 @@ export const StyledSpanOptionSelected = styled.span<{ theme: Theme; icon?: any;
}
`;

export const StyledSpanOption = styled.span<{ value: string | null }>`
export const StyledSpanOption = styled.span<{ value: string }>`
align-items: center;
padding: 2px 4px;
font-weight: 400;
Expand Down
Loading