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
11 changes: 10 additions & 1 deletion src/BaseSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ import SelectInput from '../SelectInput';
import type { ComponentsConfig } from '../hooks/useComponents';
import useComponents from '../hooks/useComponents';

export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input' | 'clear';
export type BaseSelectSemanticName =
| 'prefix'
| 'suffix'
| 'input'
| 'clear'
| 'placeholder'
| 'content'
| 'item'
| 'itemContent'
| 'itemRemove';

/**
* ZombieJ:
Expand Down
25 changes: 20 additions & 5 deletions src/SelectInput/Content/MultipleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default React.forwardRef<HTMLInputElement, SharedContentProps>(function M
maxTagPlaceholder: maxTagPlaceholderFromContext,
maxTagTextLength,
maxTagCount,
classNames,
styles,
} = useBaseProps();

const selectionItemPrefixCls = `${prefixCls}-selection-item`;
Expand Down Expand Up @@ -85,14 +87,25 @@ export default React.forwardRef<HTMLInputElement, SharedContentProps>(function M
) => (
<span
title={getTitle(item)}
className={clsx(selectionItemPrefixCls, {
[`${selectionItemPrefixCls}-disabled`]: itemDisabled,
})}
className={clsx(
selectionItemPrefixCls,
{
[`${selectionItemPrefixCls}-disabled`]: itemDisabled,
},
classNames?.item,
)}
style={styles?.item}
>
<span className={`${selectionItemPrefixCls}-content`}>{content}</span>
<span
className={clsx(`${selectionItemPrefixCls}-content`, classNames?.itemContent)}
style={styles?.itemContent}
>
{content}
</span>
{closable && (
<TransBtn
className={`${selectionItemPrefixCls}-remove`}
className={clsx(`${selectionItemPrefixCls}-remove`, classNames?.itemRemove)}
style={styles?.itemRemove}
onMouseDown={onPreventMouseDown}
onClick={onClose}
customizeIcon={removeIcon}
Expand Down Expand Up @@ -185,6 +198,8 @@ export default React.forwardRef<HTMLInputElement, SharedContentProps>(function M
return (
<Overflow
prefixCls={`${prefixCls}-content`}
className={classNames?.content}
style={styles?.content}
prefix={!displayValues.length && (!searchValue || !triggerOpen) ? <Placeholder /> : null}
data={displayValues}
renderItem={renderItem}
Expand Down
6 changes: 5 additions & 1 deletion src/SelectInput/Content/Placeholder.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from 'react';
import { clsx } from 'clsx';
import { useSelectInputContext } from '../context';
import useBaseProps from '../../hooks/useBaseProps';

export interface PlaceholderProps {
show?: boolean;
}

export default function Placeholder(props: PlaceholderProps) {
const { prefixCls, placeholder, displayValues } = useSelectInputContext();
const { classNames, styles } = useBaseProps();
const { show = true } = props;

if (displayValues.length) {
Expand All @@ -15,9 +18,10 @@ export default function Placeholder(props: PlaceholderProps) {

return (
<div
className={`${prefixCls}-placeholder`}
className={clsx(`${prefixCls}-placeholder`, classNames?.placeholder)}
style={{
visibility: show ? 'visible' : 'hidden',
...styles?.placeholder,
}}
>
{placeholder}
Expand Down
4 changes: 2 additions & 2 deletions src/SelectInput/Content/SingleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const SingleContent = React.forwardRef<HTMLInputElement, SharedContentProps>(
({ inputProps }, ref) => {
const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } =
useSelectInputContext();
const { triggerOpen, title: rootTitle, showSearch } = useBaseProps();
const { triggerOpen, title: rootTitle, showSearch, classNames, styles } = useBaseProps();
const selectContext = React.useContext(SelectContext);

const [inputChanged, setInputChanged] = React.useState(false);
Expand Down Expand Up @@ -71,7 +71,7 @@ const SingleContent = React.forwardRef<HTMLInputElement, SharedContentProps>(
}, [combobox, activeValue]);

return (
<div className={`${prefixCls}-content`}>
<div className={clsx(`${prefixCls}-content`, classNames?.content)} style={styles?.content}>
{displayValue ? (
<div {...optionProps}>{displayValue.label}</div>
) : (
Expand Down
2 changes: 1 addition & 1 deletion tests/BaseSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OptionListProps, RefOptionListProps } from '@/OptionList';
import type { OptionListProps, RefOptionListProps } from '../src/OptionList';
import { fireEvent, render } from '@testing-library/react';
import { forwardRef, act } from 'react';
import BaseSelect from '../src/BaseSelect';
Expand Down
162 changes: 162 additions & 0 deletions tests/semantic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from 'react';
import { render } from '@testing-library/react';
import Select from '../src';

describe('Select Semantic Styles', () => {
const defaultProps = {
prefixCls: 'rc-select',
displayValues: [],
emptyOptions: true,
id: 'test',
onDisplayValuesChange: () => {},
onSearch: () => {},
searchValue: '',
};

it('should apply semantic classNames correctly', () => {
const classNames = {
prefix: 'custom-prefix',
suffix: 'custom-suffix',
input: 'custom-input',
clear: 'custom-clear',
placeholder: 'custom-placeholder',
content: 'custom-content',
item: 'custom-item',
itemContent: 'custom-item-content',
itemRemove: 'custom-item-remove',
};

const { container } = render(
<Select
{...defaultProps}
classNames={classNames}
placeholder="Test placeholder"
prefix={<span>Prefix</span>}
suffix={<span>Suffix</span>}
/>,
);

// Test prefix className
expect(container.querySelector('.rc-select-prefix')).toHaveClass('custom-prefix');

// Test suffix className
expect(container.querySelector('.rc-select-suffix')).toHaveClass('custom-suffix');

// Test input className
expect(container.querySelector('.rc-select-input')).toHaveClass('custom-input');

// Test content className
expect(container.querySelector('.rc-select-content')).toHaveClass('custom-content');

// Test placeholder className
expect(container.querySelector('.rc-select-placeholder')).toHaveClass('custom-placeholder');
});

it('should apply semantic styles correctly', () => {
const styles = {
prefix: { color: 'red' },
suffix: { color: 'blue' },
input: { fontSize: '16px' },
clear: { cursor: 'pointer' },
placeholder: { opacity: 0.6 },
content: { padding: '4px' },
item: { margin: '2px' },
itemContent: { fontWeight: 'bold' },
itemRemove: { background: 'transparent' },
};

const { container } = render(
<Select
{...defaultProps}
styles={styles}
placeholder="Test placeholder"
prefix={<span>Prefix</span>}
suffix={<span>Suffix</span>}
/>,
);

// Test prefix style
expect(container.querySelector('.rc-select-prefix')).toHaveStyle({ color: 'red' });

// Test suffix style
expect(container.querySelector('.rc-select-suffix')).toHaveStyle({ color: 'blue' });

// Test input style
expect(container.querySelector('.rc-select-input')).toHaveStyle({ fontSize: '16px' });

// Test content style
expect(container.querySelector('.rc-select-content')).toHaveStyle({ padding: '4px' });

// Test placeholder style
expect(container.querySelector('.rc-select-placeholder')).toHaveStyle({ opacity: 0.6 });
});

it('should apply item semantic styles in multiple mode', () => {
const classNames = {
item: 'custom-item',
itemContent: 'custom-item-content',
itemRemove: 'custom-item-remove',
};

const styles = {
item: { margin: '2px' },
itemContent: { fontWeight: 'bold' },
itemRemove: { background: 'transparent' },
};

const displayValues = [
{ key: '1', label: 'Option 1', value: '1' },
{ key: '2', label: 'Option 2', value: '2' },
];

const { container } = render(
<Select
{...defaultProps}
mode="multiple"
value={displayValues}
classNames={classNames}
styles={styles}
/>,
);

// Test item className and style
const items = container.querySelectorAll('.rc-select-selection-item');
expect(items[0]).toHaveClass('custom-item');
expect(items[0]).toHaveStyle({ margin: '2px' });

// Test item content className and style
const itemContents = container.querySelectorAll('.rc-select-selection-item-content');
expect(itemContents[0]).toHaveClass('custom-item-content');
expect(itemContents[0]).toHaveStyle({ fontWeight: 'bold' });

// Test item remove className and style
const removeButtons = container.querySelectorAll('.rc-select-selection-item-remove');
expect(removeButtons[0]).toHaveClass('custom-item-remove');
expect(removeButtons[0]).toHaveStyle({ background: 'transparent' });
});

it('should apply clear icon semantic styles when allowClear is enabled', () => {
const classNames = {
clear: 'custom-clear',
};

const styles = {
clear: { cursor: 'pointer' },
};

const { container } = render(
<Select
{...defaultProps}
value={[{ key: '1', label: 'Option 1', value: '1' }]}
allowClear
classNames={classNames}
styles={styles}
/>,
);

// Test clear icon className and style
const clearIcon = container.querySelector('.rc-select-clear');
expect(clearIcon).toHaveClass('custom-clear');
expect(clearIcon).toHaveStyle({ cursor: 'pointer' });
});
});
Loading