From ebe4af3dab605be24db7d4fa3eee686186ef5ce8 Mon Sep 17 00:00:00 2001 From: CNWei Date: Fri, 30 Oct 2026 08:59:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20PasswordInput=20=E7=9C=BC=E7=9D=9B?= =?UTF-8?q?=E7=A7=BB=E5=85=A5=E8=BE=93=E5=85=A5=E6=A1=86=EF=BC=8C=E9=9A=90?= =?UTF-8?q?=E8=97=8F=20Edge=20=E5=8E=9F=E7=94=9F=E6=98=BE=E9=9A=90?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Input 新增 suffix 插槽,眼睛不再并排在外 - 隐藏 ::-ms-reveal / ::-ms-clear,避免 Edge 聚焦时出现两个眼睛 - 展示页补 suffix 示例;COMPONENTS.md / README 同步;版本 0.3.1 --- README.md | 2 +- docs/COMPONENTS.md | 8 +++-- package.json | 2 +- site/App.tsx | 2 +- site/sections/Inputs.tsx | 27 +++++++++++++- src/components/Input.module.css | 13 +++++++ src/components/Input.tsx | 7 +++- src/components/PasswordInput.module.css | 12 ------- src/components/PasswordInput.tsx | 47 ++++++++++++------------- 9 files changed, 76 insertions(+), 44 deletions(-) delete mode 100644 src/components/PasswordInput.module.css diff --git a/README.md b/README.md index 28cae37..8009b47 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 17bfad2..091e106 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -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 不变、仅外观变化 | diff --git a/package.json b/package.json index 90707ac..64e92b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stash/ui", - "version": "0.3.0", + "version": "0.3.1", "private": true, "description": "Stash 设计系统:设计令牌 + 基础组件 + 组件展示站", "type": "module", diff --git a/site/App.tsx b/site/App.tsx index a684133..b23bab7 100644 --- a/site/App.tsx +++ b/site/App.tsx @@ -147,7 +147,7 @@ export function App() { S Stash UI - v0.3.0 + v0.3.1 diff --git a/site/sections/Inputs.tsx b/site/sections/Inputs.tsx index d06a002..b63de77 100644 --- a/site/sections/Inputs.tsx +++ b/site/sections/Inputs.tsx @@ -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() { + } />} +/>`} + > + setHost(event.target.value)} + suffix={ + } + onClick={() => void navigator.clipboard.writeText(host)} + /> + } + style={{ width: 320 }} + /> + + void', desc: '点击清除;不传则不显示按钮' }, { name: 'shortcut', type: 'string', desc: '右侧快捷键提示(仅展示)' }, diff --git a/src/components/Input.module.css b/src/components/Input.module.css index 43e9f2d..9297002 100644 --- a/src/components/Input.module.css +++ b/src/components/Input.module.css @@ -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; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index aad38f1..5d83db5 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -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 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({ + {suffix !== undefined && {suffix}} + {shortcut !== undefined && {shortcut}} {showClear && ( diff --git a/src/components/PasswordInput.module.css b/src/components/PasswordInput.module.css deleted file mode 100644 index e8f7390..0000000 --- a/src/components/PasswordInput.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.root { - display: flex; - align-items: center; - gap: var(--su-space-2); - width: 100%; -} - -/* 输入框吃掉剩余宽度,眼睛按钮保持自身尺寸 */ -.input { - flex: 1; - min-width: 0; -} diff --git a/src/components/PasswordInput.tsx b/src/components/PasswordInput.tsx index b97dcd2..348f905 100644 --- a/src/components/PasswordInput.tsx +++ b/src/components/PasswordInput.tsx @@ -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 ( -
- - - {toggle && ( - } - onClick={() => setVisible((current) => !current)} - /> - )} -
+ } + onClick={() => setVisible((current) => !current)} + /> + ) : undefined + } + /> ) }