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