fix(ui): PasswordInput 眼睛移入输入框,隐藏 Edge 原生显隐按钮

- Input 新增 suffix 插槽,眼睛不再并排在外
- 隐藏 ::-ms-reveal / ::-ms-clear,避免 Edge 聚焦时出现两个眼睛
- 展示页补 suffix 示例;COMPONENTS.md / README 同步;版本 0.3.1
This commit is contained in:
2026-10-30 08:59:22 +08:00
parent 93b1e41d28
commit ebe4af3dab
9 changed files with 76 additions and 44 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ stash-ui/
| `Layout` | 页面骨架:页头 / 侧栏 / 内容 / 右栏 / 页脚 | `header` `sider` `aside` `footer` `maxWidth` `gap` |
| `Space` | 元素间距 | `direction` `gap` `wrap` `align` `justify` |
| `Divider` | 分隔线 | `orientation` `variant` `spacing` |
| `Input` | 输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
| `Input` | 输入框 | `icon` `suffix` `clearable` `onClear` `shortcut` `inputRef` |
| `Textarea` | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
| `PasswordInput` | 密码输入(锁图标 + 显隐切换) | `icon` `size` `toggle` |
| `Notice` | 提示条 | `tone` `title` `action` |
+5 -3
View File
@@ -188,13 +188,14 @@
- **用途**:搜索框、筛选输入、表单单行输入。
- **不该用在哪**:多行文本(未来 `Textarea`)、下拉选择(未来 `Select`)。
- **解剖**:`[icon] [input] [shortcut] [clear]`,外层容器负责边框与高度(36px、`1px solid gray-3`)。
- **解剖**:`[icon] [input] [suffix] [shortcut] [clear]`,外层容器负责边框与高度(36px、`1px solid gray-3`)。
- **Props**:
| prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `icon` | `IconName` | — | 左侧图标 |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 32 / 36 / 44px |
| `suffix` | `ReactNode` | — | 右侧插槽:图标按钮、单位等;排在清除按钮之前 |
| `clearable` | `boolean` | `false` | 有值且传了 `onClear` 才显示清除 |
| `onClear` | `() => void` | — | 清除回调 |
| `shortcut` | `string` | — | 右侧提示(如 `⌘K`),仅展示 |
@@ -202,7 +203,7 @@
| `className` / `style` | — | — | **作用在外层容器** |
| `…rest` | `InputHTMLAttributes` | — | `onChange` 是原生事件 |
- **规则**:与 `Button size="md"` 同高 36;focus 只把边框换成主色(**不再叠 outline**:文本框点击时也会命中 `:focus-visible`,叠环会出现「双框」,2026-10-28 修)。
- **规则**:与 `Button size="md"` 同高 36;focus 只把边框换成主色(**不再叠 outline**:文本框点击时也会命中 `:focus-visible`,叠环会出现「双框」,2026-10-28 修);`type="password"` 时隐藏 Edge 原生显隐按钮(`::-ms-reveal`),否则会和 `PasswordInput` 的眼睛叠成两个。
- **展示页**:`#inputs`
### 3.9 Card ✅
@@ -584,7 +585,7 @@
- **用途**:登录、改密这类密码输入;固定「锁图标 + 眼睛按钮」,显隐切换与无障碍名称由组件负责。
- **不该用在哪**:普通文本输入(用 `Input`);需要自己控制 `type` 的场景(直接用 `Input`)。
- **解剖**:外层 flex(`Input` 撑满 + 右侧 `IconButton`),`type` 在 `password` / `text` 间切换。
- **解剖**:`Input` + `suffix` 插槽里的 `IconButton`——眼睛在输入框**内部**右端,不是并排在外;`type` 在 `password` / `text` 间切换,Edge 原生显隐按钮已隐藏。
- **Props**:
| prop | 类型 | 默认 | 说明 |
@@ -698,3 +699,4 @@
| 2026-10-29 | `Breadcrumb` 增加 `onNavigate`(可选):传了就用 `preventDefault` 接管点击,交给 SPA 路由;不传保持普通链接。兼容性:新增可选 prop,无破坏性 |
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
| 2026-10-29 | 新增 `PasswordInput`(`icon` / `size` / `toggle`):锁图标 + 显隐切换 + `aria-pressed`,事件语义与 `Input` 一致。兼容性:纯新增 |
| 2026-10-30 | **修复**:`PasswordInput` 的眼睛按钮移入输入框内部(`Input` 新增 `suffix` 插槽);`Input` 隐藏 Edge 原生 `::-ms-reveal` / `::-ms-clear`,避免与自带显隐按钮叠成两个眼睛。兼容性:`Input` 新增可选 prop,无破坏性;`PasswordInput` API 不变、仅外观变化 |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@stash/ui",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"description": "Stash 设计系统:设计令牌 + 基础组件 + 组件展示站",
"type": "module",
+1 -1
View File
@@ -147,7 +147,7 @@ export function App() {
<span className="doc-brand-mark">S</span>
<span>
<strong>Stash UI</strong>
<em>v0.3.0</em>
<em>v0.3.1</em>
</span>
</a>
+26 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Button, FormField, Icon, Input, PasswordInput, Space, Text, Textarea } from '../../src'
import { Button, FormField, Icon, IconButton, Input, PasswordInput, Space, Text, Textarea } from '../../src'
import { Demo } from '../components/Demo'
import { PropsTable } from '../components/PropsTable'
import { Section } from '../components/Section'
@@ -93,6 +93,30 @@ export function Inputs() {
<Input size="lg" placeholder="大号 44(搜索框)" style={{ width: 320 }} />
</Demo>
<Demo
title="右侧插槽"
desc="suffix 放在输入框内部右端,适合放复制、显隐、单位这类单个小动作;和 clearable 同用时排在清除按钮之前。"
layout="column"
code={`<Input
placeholder="https://…"
suffix={<IconButton label="复制" size="xs" icon={<Icon name="copy_line" />} />}
/>`}
>
<Input
value={host}
onChange={(event) => setHost(event.target.value)}
suffix={
<IconButton
label="复制"
size="xs"
icon={<Icon name="copy_line" />}
onClick={() => void navigator.clipboard.writeText(host)}
/>
}
style={{ width: 320 }}
/>
</Demo>
<Demo
title="表单字段"
desc="FormField 负责「标签 + 控件 + 提示/错误」的排版;校验逻辑留在页面,错误出现时自动 role=alert。"
@@ -163,6 +187,7 @@ export function Inputs() {
rows={[
{ name: 'icon', type: 'IconName', desc: '左侧前缀图标' },
{ name: 'size', type: "'sm' | 'md' | 'lg'", defaultValue: "'md'", desc: '32 / 36 / 44px' },
{ name: 'suffix', type: 'ReactNode', desc: '右侧插槽(图标按钮、单位),排在清除按钮之前' },
{ name: 'clearable', type: 'boolean', defaultValue: 'false', desc: '有内容时显示清除按钮' },
{ name: 'onClear', type: '() => void', desc: '点击清除;不传则不显示按钮' },
{ name: 'shortcut', type: 'string', desc: '右侧快捷键提示(仅展示)' },
+13
View File
@@ -62,6 +62,19 @@
color: var(--su-text-muted);
}
.suffix {
display: inline-flex;
flex-shrink: 0;
align-items: center;
}
/* Edge 会给 type=password 的输入框塞一个原生显隐按钮(聚焦时出现),
和 PasswordInput 自己的按钮叠成两个眼睛;这里把它隐藏。 */
.input::-ms-reveal,
.input::-ms-clear {
display: none;
}
.clear {
--clear-size: 20px;
+6 -1
View File
@@ -1,4 +1,4 @@
import type { CSSProperties, InputHTMLAttributes, Ref } from 'react'
import type { CSSProperties, InputHTMLAttributes, ReactNode, Ref } from 'react'
import { Icon, type IconName } from './Icon'
import styles from './Input.module.css'
@@ -23,6 +23,8 @@ export type InputProps = {
onClear?: () => void
/** 右侧提示,如 `⌘K`;仅作展示,不参与键盘处理 */
shortcut?: string
/** 右侧插槽:放图标按钮、单位等(例如密码显隐、复制)。与 `clearable` 同时存在时排在清除按钮之前 */
suffix?: ReactNode
inputRef?: Ref<HTMLInputElement>
className?: string
style?: CSSProperties
@@ -34,6 +36,7 @@ export function Input({
clearable = false,
onClear,
shortcut,
suffix,
inputRef,
className,
style,
@@ -52,6 +55,8 @@ export function Input({
<input ref={inputRef} className={styles.input} value={value} {...rest} />
{suffix !== undefined && <span className={styles.suffix}>{suffix}</span>}
{shortcut !== undefined && <kbd className={styles.shortcut}>{shortcut}</kbd>}
{showClear && (
-12
View File
@@ -1,12 +0,0 @@
.root {
display: flex;
align-items: center;
gap: var(--su-space-2);
width: 100%;
}
/* 输入框吃掉剩余宽度,眼睛按钮保持自身尺寸 */
.input {
flex: 1;
min-width: 0;
}
+23 -24
View File
@@ -2,15 +2,17 @@ import { useState, type CSSProperties, type InputHTMLAttributes } from 'react'
import { Icon, type IconName } from './Icon'
import { IconButton } from './IconButton'
import { Input, type InputSize } from './Input'
import styles from './PasswordInput.module.css'
/**
* 密码输入框:`Input` + 右侧显隐切换。
* 密码输入框:`Input` + 输入框内部的显隐切换。
*
* 为什么单独成组件而不是每个页面拼一遍:密码框的「锁图标 + 眼睛按钮 +
* aria 名称 + type 切换」是固定搭配,手拼时很容易漏掉显隐按钮(Stash 的
* 系统改密页就漏过一次),而漏掉之后用户无法确认自己输的是什么。
*
* 眼睛放在输入框内部(`Input` 的 suffix 槽)而不是外面并排:
* 并排时会和 Edge 原生的密码显隐按钮叠成两个眼睛,视觉上也更像一个控件。
*
* 事件语义与 `Input` 一致:`onChange` 收到的是原生事件,Enter 等键盘逻辑
* 用 `onKeyDown`。输入框本身不知道「密码是否正确」这类业务状态。
*/
@@ -39,28 +41,25 @@ export function PasswordInput({
}: PasswordInputProps) {
const [visible, setVisible] = useState(false)
// 眼睛按钮的高度跟随输入框,避免并排时上下错位
const buttonSize = size === 'lg' ? 'md' : size === 'sm' ? 'xs' : 'sm'
return (
<div className={[styles.root, className].filter(Boolean).join(' ')} style={style}>
<Input
{...rest}
className={styles.input}
size={size}
type={visible ? 'text' : 'password'}
icon={icon ?? undefined}
/>
{toggle && (
<IconButton
label={visible ? '隐藏密码' : '显示密码'}
aria-pressed={visible}
size={buttonSize}
icon={<Icon name={visible ? 'eye_close_line' : 'eye_line'} />}
onClick={() => setVisible((current) => !current)}
/>
)}
</div>
<Input
{...rest}
className={className}
style={style}
size={size}
type={visible ? 'text' : 'password'}
icon={icon ?? undefined}
suffix={
toggle ? (
<IconButton
label={visible ? '隐藏密码' : '显示密码'}
aria-pressed={visible}
size="xs"
icon={<Icon name={visible ? 'eye_close_line' : 'eye_line'} />}
onClick={() => setVisible((current) => !current)}
/>
) : undefined
}
/>
)
}