Skip to content
Open
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
8 changes: 8 additions & 0 deletions docs/demo/select-content-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: select-content-option
nav:
title: Demo
path: /demo
---

<code src="../examples/select-content-option.tsx"></code>
186 changes: 186 additions & 0 deletions docs/examples/select-content-option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from 'react';
import Select, { Option } from '@rc-component/select';
import '../../assets/index.less';

const Demo: React.FC = () => {
const [value, setValue] = React.useState<string>('red');
const [dynamicOptions, setDynamicOptions] = React.useState<
{ value: string; label: string; style?: React.CSSProperties }[]
>([
{
value: 'custom-1',
label: 'Custom Option 1',
style: { color: '#1890ff' },
},
{
value: 'custom-2',
label: 'Custom Option 2',
style: { color: '#52c41a' },
},
]);

const handleSearch = (searchValue: string) => {
if (searchValue && !dynamicOptions.find((opt) => opt.value === searchValue)) {
setDynamicOptions([
...dynamicOptions,
{
value: searchValue,
label: searchValue,
style: { color: '#faad14' },
},
]);
Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For state updates that depend on the previous state, it's a best practice to use the functional update form of useState's setter function. This avoids potential issues with stale state, for example if handleSearch were to be called in quick succession.

Suggested change
setDynamicOptions([
...dynamicOptions,
{
value: searchValue,
label: searchValue,
style: { color: '#faad14' },
},
]);
setDynamicOptions((prevDynamicOptions) => [
...prevDynamicOptions,
{
value: searchValue,
label: searchValue,
style: { color: '#faad14' },
},
]);

}
};
Comment on lines +22 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

handleSearch 中存在闭包陈旧引用问题

setDynamicOptions 使用了展开当前 dynamicOptions 的方式,在快速连续输入时可能引用到过时的 state。建议使用函数式更新:

♻️ 建议使用函数式更新
   const handleSearch = (searchValue: string) => {
-    if (searchValue && !dynamicOptions.find((opt) => opt.value === searchValue)) {
-      setDynamicOptions([
-        ...dynamicOptions,
+    if (searchValue) {
+      setDynamicOptions((prev) => {
+        if (prev.find((opt) => opt.value === searchValue)) return prev;
+        return [
+        ...prev,
         {
           value: searchValue,
           label: searchValue,
           style: { color: '#faad14' },
         },
-      ]);
+        ];
+      });
     }
   };
🤖 Prompt for AI Agents
In `@docs/examples/select-content-option.tsx` around lines 22 - 33, handleSearch
has a stale closure risk because it spreads the current dynamicOptions when
calling setDynamicOptions; change setDynamicOptions to use the functional
updater form to avoid reading an out‑of‑date dynamicOptions (i.e., call
setDynamicOptions(prev => { check prev for existing value and return [...prev,
newOption] }) while keeping the same new option shape and the existing
early-exit check logic; update references inside handleSearch to use the prev
argument instead of dynamicOptions.


return (
<div style={{ margin: 20 }}>
<h2>Option Style & ClassName for Selected Item</h2>
<p>
When an option has <code>style</code> or <code>className</code> props, they will be applied
to the selected item display.
</p>

<div style={{ marginBottom: 24 }}>
<h3>Basic Usage with Style</h3>
<Select
value={value}
style={{ width: 200 }}
onChange={(val) => setValue(val as string)}
options={[
{
value: 'red',
label: 'Red Color',
style: { color: 'red', fontWeight: 'bold' },
},
{
value: 'blue',
label: 'Blue Color',
style: { color: 'blue', fontWeight: 'bold' },
},
{
value: 'green',
label: 'Green Color',
style: { color: 'green', fontWeight: 'bold' },
},
{
value: 'normal',
label: 'Normal (no style)',
},
]}
/>
</div>

<div style={{ marginBottom: 24 }}>
<h3>With ClassName</h3>
<Select
defaultValue="styled"
style={{ width: 200 }}
options={[
{
value: 'styled',
label: 'Styled Option',
className: 'custom-option-class',
style: { background: '#e6f7ff', border: '1px solid #1890ff' },
},
{
value: 'normal',
label: 'Normal Option',
},
]}
/>
</div>

<div style={{ marginBottom: 24 }}>
<h3>With Title (Tooltip)</h3>
<Select
defaultValue="with-title"
style={{ width: 200 }}
options={[
{
value: 'with-title',
label: 'Hover me!',
title: 'This is a custom tooltip for this option',
style: { color: 'purple' },
},
{
value: 'without-title',
label: 'No Title',
style: { color: 'orange' },
},
]}
/>
</div>

<div style={{ marginBottom: 24 }}>
<h3>Using Option Children Syntax</h3>
<Select defaultValue="option1" style={{ width: 200 }}>
<Option
value="option1"
style={{ color: 'orange' }}
className="custom-className"
title="Custom title for option 1"
>
Option 1
</Option>
<Option value="option2" style={{ color: 'pink' }}>
Option 2
</Option>
<Option value="option3">Option 3 (no style)</Option>
</Select>
</div>

<div style={{ marginBottom: 24 }}>
<h3>Root Title Override</h3>
<Select
value="override"
title="This title overrides option title"
style={{ width: 200 }}
options={[
{
value: 'override',
label: 'Hover to see title',
title: 'Option title (will be overridden)',
style: { color: 'teal' },
},
]}
/>
<h3>Custom Input Element with getInputElement (combobox mode)</h3>
<div style={{ marginBottom: 24 }}>
<p>
Use <code>getInputElement</code> to customize the input element. This only works with{' '}
<code>mode=&quot;combobox&quot;</code>. Type to add new options dynamically.
</p>
<Select
mode="combobox"
style={{ width: 300 }}
suffixIcon={null}
showSearch
onSearch={handleSearch}
classNames={{
prefix: 'custom-prefix',
suffix: 'custom-suffix',
}}
styles={{
prefix: { marginRight: 8 },
suffix: { marginLeft: 8 },
}}
getInputElement={() => (
<input
style={{
border: '2px solid #1890ff',
borderRadius: 4,
padding: '4px 8px',
outline: 'none',
}}
placeholder="Type to add new option"
/>
)}
options={dynamicOptions}
/>
</div>
</div>
Comment on lines +132 to +181
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Custom Input Element" 部分嵌套在 "Root Title Override" 的 div 内

Line 147 的 <h3>Custom Input Element...</h3> 及其内容位于 Line 132 开始的 "Root Title Override" div 中(该 div 在 Line 181 关闭)。这两个是独立的 demo 部分,应该各自有自己的容器 div。

♻️ 建议拆分为独立的 section
         />
+      </div>
+
+      <div style={{ marginBottom: 24 }}>
         <h3>Custom Input Element with getInputElement (combobox mode)</h3>

同时移除 Line 181 多余的 </div> 闭合标签。

🤖 Prompt for AI Agents
In `@docs/examples/select-content-option.tsx` around lines 132 - 181, The "Custom
Input Element" demo block is accidentally nested inside the "Root Title
Override" container: the <h3>Custom Input Element...</h3> and its Select
(mode="combobox", getInputElement, options={dynamicOptions}) are inside the same
outer div that wraps the Root Title Override Select; split them into two sibling
container divs so each demo has its own wrapper and remove the extra closing
</div> that currently closes the Root Title Override wrapper incorrectly; locate
the two Select usages and their surrounding <div> blocks to separate them into
distinct sections.

</div>
);
};

export default Demo;
24 changes: 10 additions & 14 deletions src/SelectInput/Content/SingleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,16 @@ const SingleContent = React.forwardRef<HTMLInputElement, SharedContentProps>(

// Render value
const renderValue = displayValue ? (
hasOptionStyle ? (
<div
className={clsx(`${prefixCls}-content-value`, optionClassName)}
style={{
...(mergedSearchValue ? { visibility: 'hidden' } : {}),
...optionStyle,
}}
title={optionTitle}
>
{displayValue.label}
</div>
) : (
displayValue.label
)
<div
className={clsx(`${prefixCls}-content-value`, optionClassName)}
style={{
...(mergedSearchValue ? { visibility: 'hidden' } : {}),
...optionStyle,
}}
title={optionTitle}
>
{displayValue.label}
</div>
Comment on lines +74 to +83
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

hasOptionStyle 为 false 时,title 同时出现在内层 div 和外层 content div 上

Line 80 始终将 optionTitle 设置在内层 .rc-select-content-value div 上,而 Line 99 在 !hasOptionStyle 时也将 optionTitle 设置在外层 .rc-select-content div 上。这会导致在没有 option style 的情况下,两个嵌套元素都有 title 属性,tooltip 可能出现重复。

考虑仅在 hasOptionStyle 为 true 时才在内层 div 设置 title,与外层 content div 的逻辑互斥。

Also applies to: 99-99

🤖 Prompt for AI Agents
In `@src/SelectInput/Content/SingleContent.tsx` around lines 74 - 83, The inner
value div (`.rc-select-content-value`) always gets title={optionTitle} causing
duplicate tooltips when the outer content div (`.rc-select-content`) also sets
title in the !hasOptionStyle branch; update the JSX in SingleContent.tsx so the
inner div only receives the title when hasOptionStyle is true (i.e. make the
title on the inner element conditional on hasOptionStyle), keeping the outer
content div's existing logic unchanged; reference the hasOptionStyle flag,
optionTitle variable, the inner element rendering displayValue.label and the
optionClassName/optionStyle props to locate and adjust the title assignment.

⚠️ Potential issue | 🟡 Minor

optionStyle 可能覆盖搜索时的 visibility: 'hidden'

mergedSearchValue 为真时,visibility: 'hidden' 先被展开,但随后 optionStyle 被展开在后面。如果 option 的 style 中包含 visibility 属性,会覆盖掉隐藏逻辑,导致搜索时选中值仍然可见。

建议将 optionStyle 放在 visibility 之前展开,确保搜索隐藏逻辑优先:

🐛 建议修复样式合并顺序
       style={{
+        ...optionStyle,
         ...(mergedSearchValue ? { visibility: 'hidden' } : {}),
-        ...optionStyle,
       }}
🤖 Prompt for AI Agents
In `@src/SelectInput/Content/SingleContent.tsx` around lines 74 - 83, The current
JSX spreads mergedSearchValue visibility before optionStyle so optionStyle can
override it; in SingleContent.tsx adjust the spread order so optionStyle is
applied first and then the conditional visibility from mergedSearchValue is
spread (or explicitly set visibility when mergedSearchValue is true) on the div
that uses prefixCls-content-value, optionClassName, optionStyle,
mergedSearchValue, optionTitle and displayValue.label to ensure the search-hide
logic always wins.

) : (
<Placeholder show={!mergedSearchValue} />
);
Expand Down
Loading