- 前端
- UI组件
【免费下载链接】rsuite
🧱 A suite of React components .
导读
在 rsuite 中,InputGroup用于将输入框与按钮、附加元素(Addon)组合成一个整体控件。本文围绕官方文档 InputGroup 与 Dropdown 组合示例,完整拆解如何借助Whisper+Popover+Dropdown实现一个带货币下拉选择的输入框,并对比inside内外两种形态的差异。读完本文,你将掌握InputGroup的组件层级、Whisper自定义弹出面板的用法,以及如何将下拉菜单无缝嵌入输入框组合中,可直接复用到汇率换算、单位选择、搜索筛选等场景。
示例总览:货币选择输入框
该示例实现了一个典型的"下拉 + 输入 + 附加符号"三段式组合:左侧按钮显示当前货币代码(如CNY)并可点击弹出货币列表,中间是金额输入框,右侧 Addon 显示货币符号(如¥)。核心代码如下(完整示例见 input-group-dropdown.md):
import { InputGroup, Dropdown, Whisper, Popover, Input, VStack, Box } from 'rsuite'; const currencies = [ { label: 'CNY - Chinese Yuan', value: 'CNY', symbol: '¥', flag: '🇨🇳' }, { label: 'USD - US Dollar', value: 'USD', symbol: '$', flag: '🇺🇸' }, { label: 'EUR - Euro', value: 'EUR', symbol: '€', flag: '🇪🇺' }, { label: 'GBP - British Pound', value: 'GBP', symbol: '£', flag: '🇬🇧' }, { label: 'JPY - Japanese Yen', value: 'JPY', symbol: '¥', flag: '🇯🇵' } ]; function App() { const [currency, setCurrency] = React.useState(currencies[0]); const renderMenu = ({ onClose, left, top, className }, ref) => { return ( <Popover ref={ref} className={className} full> <Dropdown.Menu> {currencies.map(currency => ( <Dropdown.Item key={currency.value} eventKey={currency.value} onSelect={() => { setCurrency(currency); onClose(); }} > <Box as="span" mr={8}> {currency.flag} </Box> {currency.label} </Dropdown.Item> ))} </Dropdown.Menu> </Popover> ); }; return ( <VStack w={300}> <InputGroup> <Whisper placement="bottomStart" trigger="click" speaker={renderMenu}> <InputGroup.Button> <HStack> {currency.value} <ArrowDownIcon /> </HStack> </InputGroup.Button> </Whisper> <Input /> <InputGroup.Addon>{currency.symbol}</InputGroup.Addon> </InputGroup> <InputGroup inside> <Whisper placement="bottomStart" trigger="click" speaker={renderMenu}> <InputGroup.Button> <HStack> {currency.value} <ArrowDownIcon /> </HStack> </InputGroup.Button> </Whisper> <Input /> <InputGroup.Addon>{currency.symbol}</InputGroup.Addon> </InputGroup> </VStack> ); } ReactDOM.render(<App />, document.getElementById('root'));示例同时渲染了两种形态:默认(带边框分段式)与inside(按钮内嵌于输入框内部),两者共用同一个renderMenu弹出面板,方便对比视觉效果。
组合结构拆解:InputGroup 的组件层级
在深入细节前,先明确InputGroup的组成。官方组件文档(Input 组件页)定义了以下三个可组合子组件:
<Input>:单行文本输入框;<InputGroup>:输入框组合容器,支持inside、disabled、size等属性;<InputGroup.Button>:与输入框组合的按钮,继承<Button>的全部属性;<InputGroup.Addon>:自定义附加元素(如单位、图标、货币符号)。
从源码看,InputGroup是一个复合组件,通过静态属性挂载子组件(InputGroup.tsx):
const Subcomponents = { Addon: InputGroupAddon, Button: InputGroupButton };同时,InputGroup通过InputGroupContext(InputGroupContext.tsx)向子元素下发size与焦点回调:
const contextValue = useMemo( () => ({ size, onFocus: handleFocus, onBlur: handleBlur }), [size, handleFocus, handleBlur] );并且InputGroup内部会监听focus/blur,通过data-focus属性驱动整个组合的聚焦高亮样式(styles/index.scss 中&:focus-within规则)。因此,无论你往组合里塞入 Button、Addon 还是输入框,整个组的尺寸、边框、聚焦态都会自动保持一致。
在示例中,InputGroup内依次排列了三个子节点:
Whisper包裹的InputGroup.Button(货币选择按钮);Input(金额输入框);InputGroup.Addon(当前货币符号)。
InputGroup采用display: flex布局(源码中position: relative; display: flex; overflow: hidden;),内部> .rs-input会flex: 1 1 auto自动撑满剩余宽度,所以三个子节点天然形成"按钮 + 输入 + 符号"的一体化排列,无需手动布局。
用 Whisper + Popover 自定义下拉弹出层
示例的核心技巧在于:没有使用Dropdown的默认触发器,而是用Whisper包裹InputGroup.Button,并将Dropdown.Menu放进Popover作为自定义 speaker。
Whisper的关键属性:
trigger="click":点击按钮时弹出;placement="bottomStart":面板从按钮左下角对齐展开;speaker={renderMenu}:渲染弹出内容。当传入函数时,函数签名是({ onClose, left, top, className }, ref),即 Whisper 会把定位信息、关闭回调和样式类传给自定义面板(相关实现见 Whisper.tsx 中placement、onClose的传递逻辑)。
const renderMenu = ({ onClose, left, top, className }, ref) => ( <Popover ref={ref} className={className} full> <Dropdown.Menu> {currencies.map(currency => ( <Dropdown.Item key={currency.value} eventKey={currency.value} onSelect={() => { setCurrency(currency); onClose(); }} > <Box as="span" mr={8}>{currency.flag}</Box> {currency.label} </Dropdown.Item> ))} </Dropdown.Menu> </Popover> );这里有几处值得注意的细节:
Popover full:full让 Popover 完全由内容撑开、不做宽度收缩,适合菜单这种可变宽度内容;className透传:必须把 Whisper 传入的className挂到Popover上,否则定位/动画相关样式无法生效;Dropdown.Menu与Dropdown.Item:复用 rsuite 的标准菜单结构与键盘交互(eventKey标识选项),Dropdown.Item的onSelect中先setCurrency(currency)更新选中值,再调用onClose()关闭弹出层,实现"选择即关闭"的下拉行为;Box用于内联图标:<Box as="span" mr={8}>为国旗 emoji 提供间距,等价于span+margin-right: 8px。
两种形态对比:普通 InputGroup 与 inside
示例下半部分将同样的三段结构放进<InputGroup inside>,这是两种主流视觉形态:
| 对比项 | 普通InputGroup | inside(内嵌) |
|---|---|---|
| 按钮/Addon 位置 | 与输入框并列,形成分段式边框 | 内嵌在输入框内部,视觉上更紧凑 |
| 样式依据 | 默认分组样式 | data-inside="true"触发 styles/index.scss 中&[data-inside='true']规则 |
| 适用场景 | 传统 Bootstrap 风格的分段组合 | 现代"搜索框带按钮/单位"风格,例如搜索、筛选、货币输入 |
从源码看,inside模式下按钮和 Addon 会获得透明背景与内边距调整:
&[data-inside='true'] { align-items: center; background-color: var(--rs-input-bg); .rs-input-group-btn { background-color: transparent; margin-inline: var(--rs-input-group-inside-btn-spacing); padding-inline: var(--rs-input-group-inside-btn-padding); } .rs-input-group-addon { background: none; border: none; } }同时,inside模式会自动处理输入框两端的内边距(当左右存在元素时消除输入框对应侧的 padding),保证内容不与内嵌元素重叠(源码中padding-inline-start: 0/padding-inline-end: 0规则)。InputGroup的data-inside与data-size、data-disabled一样,是组件内部用于样式分发的数据属性(见 InputGroup.tsx 中的data-inside={inside})。
关键 Props 速查表
<InputGroup>
| 属性 | 类型(默认值) | 说明 |
|---|---|---|
| as | ElementType('div') | 自定义渲染的 HTML 元素 |
| classPrefix | string('input-group') | CSS 类名前缀 |
| disabled | boolean | 组合整体禁用(会向子元素传递disabled,见 InputGroup.tsx 中React.cloneElement逻辑) |
| inside | boolean | 组合内容内嵌模式 |
| size | 'lg' | 'md' | 'sm' | 'xs'('md') | 组合尺寸,与 Input 的size对应 |
<InputGroup.Button>
| 属性 | 类型(默认值) | 说明 |
|---|---|---|
| classPrefix | string('input-group-btn') | CSS 类名前缀 |
| ... | ButtonProps | 继承<Button>全部属性 |
源码中InputGroupButton本质是套用input-group-btn类名的<Button>(InputGroupButton.tsx),因此appearance、size、onClick等按钮属性均可直接使用。
<InputGroup.Addon>
| 属性 | 类型(默认值) | 说明 |
|---|---|---|
| as | ElementType('span') | 自定义渲染的 HTML 元素 |
| classPrefix | string('input-group-addon') | CSS 类名前缀 |
| disabled | boolean | Addon 单独禁用态(InputGroupAddon.tsx) |
Whisper 关键属性(本示例用到)
| 属性 | 值 | 说明 |
|---|---|---|
| trigger | 'click' | 触发方式(点击弹出) |
| placement | 'bottomStart' | 弹出面板对齐位置 |
| speaker | 组件或({ onClose, left, top, className }, ref) => ReactNode | 弹出内容,函数形式可接收定位与关闭回调 |
数据流与关闭逻辑
整个示例的状态管理非常简单:currency作为唯一状态保存在App中,通过useState初始化第一个货币CNY。
选择流程如下:
- 用户点击
InputGroup.Button,Whisper 触发speaker渲染Popover+Dropdown.Menu; - 用户点击某个
Dropdown.Item,触发onSelect; setCurrency(currency)更新按钮文本(currency.value)与右侧 Addon 符号(currency.symbol);onClose()关闭弹出层。
由于按钮、输入框和 Addon 都在同一个InputGroup内,状态更新后三段内容会同步刷新,呈现"货币代码 → 输入金额 → 货币符号"的完整语义。
测试与样式佐证
仓库为InputGroup提供了组件级测试(InputGroup.test.tsx、InputGroup.spec.tsx)与样式级测试(InputGroup.styles.spec.tsx),测试中还演示了InputGroup.Addon通过as="label"与htmlFor配合原生<input>的用法,说明 Addon 支持自定义元素类型,可用于无障碍标签场景:
<InputGroup> <InputGroup.Addon as="label" htmlFor="input"></InputGroup.Addon> <input id="input" /> </InputGroup>样式层面,styles/index.scss 定义了 xs/sm/md/lg 四档尺寸变量(通过data-size选择)、聚焦态(focus-ring+ 边框高亮)、禁用态(data-disabled="true"时灰底 +cursor: not-allowed)以及子组件去边框规则(.rs-input、.rs-input-group-btn、.rs-input-group-addon均移除自身边框与圆角),这些保证了无论组合里放多少个元素,整体始终呈现为一个圆角统一的控件。
扩展思路
基于该组合模式,可以轻松演变出更多交互:
- 单位选择输入:把货币列表换成
kg / lb、px / rem等单位,右侧 Addon 显示对应单位; - 搜索筛选:左侧用
Dropdown切换搜索范围(如全部 / 标题 / 作者),中间输入关键词,右侧 Addon 放搜索图标,配合inside形态即为典型搜索框; - 受控外部菜单:如果希望菜单内容由父级动态控制,可将
Dropdown.Menu替换为自定义列表,只要保留onClose与定位参数的传递即可; - 多列/分组菜单:利用
Dropdown.Menu支持的分组与Dropdown.Item的disabled等属性,可构建更复杂的选项面板。
需要注意的是,Dropdown的默认触发器(DropdownToggle)并不适合直接嵌入InputGroup,官方示例选择的"Whisper 手动触发 + 自定义 speaker"方案正是为了让弹出层与输入组合共用定位与关闭机制,这是本模式最值得复用的设计点。
总结
通过本文可以确认:InputGroup是 rsuite 中一个"组合即结构"的容器组件,其Button/Addon子组件与 Context 机制保证了多元素组合的尺寸与状态同步;而"Whisper + Popover + Dropdown.Menu"的组合方案则为InputGroup注入了灵活的下拉能力。理解inside属性与data-*样式分发机制后,你可以在 Input 文档 所列 Props 的基础上,自由搭建出符合业务需求的分段式或内嵌式组合输入控件。
- 前端
- UI组件
【免费下载链接】rsuite
🧱 A suite of React components .
相关推荐
RSUITE Dropdown 菜单项进阶:icon、shortcut 与 description 的组合实战
RSUITE Dropdown 菜单项进阶:icon、shortcut 与 description 的组合实战 本篇技术指南围绕 rsuite 官方文档中的 D
前端UI组件RSUITE AutoComplete 与 InputGroup 组合实战:构建带搜索按钮与前后缀图标的自动补全输入框
RSUITE AutoComplete 与 InputGroup 组合实战:构建带搜索按钮与前后缀图标的自动补全输入框 AutoComplete 是 RSUIT
前端UI组件Ant Design Dropdown 按钮式下拉菜单(Button with dropdown menu)组合实战指南
Ant Design Dropdown 按钮式下拉菜单(Button with dropdown menu)组合实战指南 本指南围绕 ant design 官方
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考