feat(input): add show-password-toggle prop to support show & hide password (#179)

* feat(n-input): add show-password attribute to support show & hide password

* feat(n-input): update show-password-toggle attribute

* docs: fix typos

* feat(n-input): remove unnecessary code

* feat(n-input): trigger click to show & hide password

* feat(n-input): add icon component prop onMouseup

* style(icon): add style to the eye icon

* style(n-input): modify css
This commit is contained in:
kalykun 2021-06-18 21:23:08 +08:00 committed by GitHub
parent faee18c1d4
commit 96ae3a38dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 135 additions and 9 deletions

View File

@ -2,6 +2,8 @@
## Pending ## Pending
- `n-input` add show-password-toggle prop.
### Feats ### Feats
- `n-form`, `n-form-item` enhance show-require-mark propcloses [#171](https://github.com/TuSimple/naive-ui/issues/171) - `n-form`, `n-form-item` enhance show-require-mark propcloses [#171](https://github.com/TuSimple/naive-ui/issues/171)

View File

@ -2,6 +2,8 @@
## Pending ## Pending
- 为 `n-input` 组件中 password 属性增加查看隐藏功能
### Feats ### Feats
- `n-form`, `n-form-item` 增强 show-require-mark 属性,关闭 [#171](https://github.com/TuSimple/naive-ui/issues/171) - `n-form`, `n-form-item` 增强 show-require-mark 属性,关闭 [#171](https://github.com/TuSimple/naive-ui/issues/171)

View File

@ -20,7 +20,8 @@ export default defineComponent({
required: true required: true
}, },
onClick: Function as PropType<(e: MouseEvent) => void>, onClick: Function as PropType<(e: MouseEvent) => void>,
onMousedown: Function as PropType<(e: MouseEvent) => void> onMousedown: Function as PropType<(e: MouseEvent) => void>,
onMouseup: Function as PropType<(e: MouseEvent) => void>
}, },
setup (props) { setup (props) {
useStyle('BaseIcon', style, toRef(props, 'clsPrefix')) useStyle('BaseIcon', style, toRef(props, 'clsPrefix'))
@ -31,6 +32,7 @@ export default defineComponent({
class={`${this.clsPrefix}-base-icon`} class={`${this.clsPrefix}-base-icon`}
onClick={this.onClick} onClick={this.onClick}
onMousedown={this.onMousedown} onMousedown={this.onMousedown}
onMouseup={this.onMouseup}
role={this.role} role={this.role}
aria-label={this.ariaLabel} aria-label={this.ariaLabel}
aria-hidden={this.ariaHidden} aria-hidden={this.ariaHidden}

View File

@ -0,0 +1,28 @@
import { h, defineComponent } from 'vue'
export default defineComponent({
name: 'Eye',
render () {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
d="M255.66 112c-77.94 0-157.89 45.11-220.83 135.33a16 16 0 0 0-.27 17.77C82.92 340.8 161.8 400 255.66 400c92.84 0 173.34-59.38 221.79-135.25a16.14 16.14 0 0 0 0-17.47C428.89 172.28 347.8 112 255.66 112z"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="32"
/>
<circle
cx="256"
cy="256"
r="80"
fill="none"
stroke="currentColor"
stroke-miterlimit="10"
stroke-width="32"
/>
</svg>
)
}
})

View File

@ -0,0 +1,31 @@
import { h, defineComponent } from 'vue'
export default defineComponent({
name: 'EyeOff',
render () {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path
d="M432 448a15.92 15.92 0 0 1-11.31-4.69l-352-352a16 16 0 0 1 22.62-22.62l352 352A16 16 0 0 1 432 448z"
fill="currentColor"
/>
<path
d="M255.66 384c-41.49 0-81.5-12.28-118.92-36.5c-34.07-22-64.74-53.51-88.7-91v-.08c19.94-28.57 41.78-52.73 65.24-72.21a2 2 0 0 0 .14-2.94L93.5 161.38a2 2 0 0 0-2.71-.12c-24.92 21-48.05 46.76-69.08 76.92a31.92 31.92 0 0 0-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416a239.13 239.13 0 0 0 75.8-12.58a2 2 0 0 0 .77-3.31l-21.58-21.58a4 4 0 0 0-3.83-1a204.8 204.8 0 0 1-51.16 6.47z"
fill="currentColor"
/>
<path
d="M490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96a227.34 227.34 0 0 0-74.89 12.83a2 2 0 0 0-.75 3.31l21.55 21.55a4 4 0 0 0 3.88 1a192.82 192.82 0 0 1 50.21-6.69c40.69 0 80.58 12.43 118.55 37c34.71 22.4 65.74 53.88 89.76 91a.13.13 0 0 1 0 .16a310.72 310.72 0 0 1-64.12 72.73a2 2 0 0 0-.15 2.95l19.9 19.89a2 2 0 0 0 2.7.13a343.49 343.49 0 0 0 68.64-78.48a32.2 32.2 0 0 0-.1-34.78z"
fill="currentColor"
/>
<path
d="M256 160a95.88 95.88 0 0 0-21.37 2.4a2 2 0 0 0-1 3.38l112.59 112.56a2 2 0 0 0 3.38-1A96 96 0 0 0 256 160z"
fill="currentColor"
/>
<path
d="M165.78 233.66a2 2 0 0 0-3.38 1a96 96 0 0 0 115 115a2 2 0 0 0 1-3.38z"
fill="currentColor"
/>
</svg>
)
}
})

View File

@ -7,6 +7,8 @@ export { default as CheckmarkIcon } from './Checkmark'
export { default as ChevronLeftIcon } from './ChevronLeft' export { default as ChevronLeftIcon } from './ChevronLeft'
export { default as ChevronRightIcon } from './ChevronRight' export { default as ChevronRightIcon } from './ChevronRight'
export { default as CloseIcon } from './Close' export { default as CloseIcon } from './Close'
export { default as EyeIcon } from './Eye'
export { default as EyeOffIcon } from './EyeOff'
export { default as TrashIcon } from './Trash' export { default as TrashIcon } from './Trash'
export { default as DownloadIcon } from './Download' export { default as DownloadIcon } from './Download'
export { default as EmptyIcon } from './Empty' export { default as EmptyIcon } from './Empty'

View File

@ -30,6 +30,7 @@ count
| clearable | `boolean` | `false` | | | clearable | `boolean` | `false` | |
| default-value | `string \| [string, string] \| null` | `null` | | | default-value | `string \| [string, string] \| null` | `null` | |
| disabled | `boolean` | `false` | | | disabled | `boolean` | `false` | |
| show-password-toggle | `boolean` | `false` | Controls the display and hiding of passwords |
| maxlength | `number` | `undefined` | | | maxlength | `number` | `undefined` | |
| minlength | `number` | `undefined` | | | minlength | `number` | `undefined` | |
| pair | `boolean` | `false` | Whether to input pairwise value. | | pair | `boolean` | `false` | Whether to input pairwise value. |

View File

@ -1,5 +1,10 @@
# Password # Password
```html ```html
<n-input type="password" placeholder="Password" :maxlength="8" /> <n-input
type="password"
show-password-toggle
placeholder="Password"
:maxlength="8"
/>
``` ```

View File

@ -30,6 +30,7 @@ count
| clearable | `boolean` | `false` | | | clearable | `boolean` | `false` | |
| default-value | `string \| [string, string] \| null` | `null` | | | default-value | `string \| [string, string] \| null` | `null` | |
| disabled | `boolean` | `false` | | | disabled | `boolean` | `false` | |
| show-password-toggle | `boolean` | `false` | 控制密码的显示隐藏 |
| maxlength | `number` | `undefined` | | | maxlength | `number` | `undefined` | |
| minlength | `number` | `undefined` | | | minlength | `number` | `undefined` | |
| pair | `boolean` | `false` | 是否输入成对的值 | | pair | `boolean` | `false` | 是否输入成对的值 |

View File

@ -1,5 +1,10 @@
# 密码 # 密码
```html ```html
<n-input type="password" placeholder="密码" :maxlength="8" /> <n-input
type="password"
show-password-toggle
placeholder="密码"
:maxlength="8"
/>
``` ```

View File

@ -18,7 +18,8 @@ import {
import { useMergedState } from 'vooks' import { useMergedState } from 'vooks'
import { toRgbString, getAlphaString, getPadding } from 'seemly' import { toRgbString, getAlphaString, getPadding } from 'seemly'
import { VResizeObserver } from 'vueuc' import { VResizeObserver } from 'vueuc'
import { NBaseClear } from '../../_internal' import { NBaseClear, NBaseIcon } from '../../_internal'
import { EyeIcon, EyeOffIcon } from '../../_internal/icons'
import { useTheme, useLocale, useFormItem, useConfig } from '../../_mixins' import { useTheme, useLocale, useFormItem, useConfig } from '../../_mixins'
import type { ThemeProps } from '../../_mixins' import type { ThemeProps } from '../../_mixins'
import { call, createKey, ExtractPublicPropTypes } from '../../_utils' import { call, createKey, ExtractPublicPropTypes } from '../../_utils'
@ -75,6 +76,7 @@ const inputProps = {
default: false default: false
}, },
passivelyActivated: Boolean, passivelyActivated: Boolean,
showPasswordToggle: Boolean,
stateful: { stateful: {
type: Boolean, type: Boolean,
default: true default: true
@ -220,6 +222,8 @@ export default defineComponent({
return !!mergedValue && (hoverRef.value || mergedFocus) return !!mergedValue && (hoverRef.value || mergedFocus)
} }
}) })
// passwordVisible
const passwordVisibleRef = ref<boolean>(false)
// focus // focus
const mergedFocusRef = computed(() => { const mergedFocusRef = computed(() => {
return props.internalForceFocus || focusedRef.value return props.internalForceFocus || focusedRef.value
@ -495,6 +499,15 @@ export default defineComponent({
function handleMouseLeave (): void { function handleMouseLeave (): void {
hoverRef.value = false hoverRef.value = false
} }
function handlePasswordToggleClick (): void {
passwordVisibleRef.value = !passwordVisibleRef.value
}
function handlePasswordToggleMousedown (e: MouseEvent): void {
e.preventDefault()
}
function handlePasswordToggleMouseup (e: MouseEvent): void {
e.preventDefault()
}
function handleWrapperKeyDown (e: KeyboardEvent): void { function handleWrapperKeyDown (e: KeyboardEvent): void {
props.onKeydown?.(e) props.onKeydown?.(e)
switch (e.code) { switch (e.code) {
@ -630,6 +643,7 @@ export default defineComponent({
// value // value
uncontrolledValue: uncontrolledValueRef, uncontrolledValue: uncontrolledValueRef,
mergedValue: mergedValueRef, mergedValue: mergedValueRef,
passwordVisible: passwordVisibleRef,
mergedPlaceholder: mergedPlaceholderRef, mergedPlaceholder: mergedPlaceholderRef,
showPlaceholder1: showPlaceholder1Ref, showPlaceholder1: showPlaceholder1Ref,
showPlaceholder2: showPlaceholder2Ref, showPlaceholder2: showPlaceholder2Ref,
@ -641,6 +655,9 @@ export default defineComponent({
textDecorationStyle: textDecorationStyleRef, textDecorationStyle: textDecorationStyleRef,
mergedClsPrefix: mergedClsPrefixRef, mergedClsPrefix: mergedClsPrefixRef,
mergedBordered: mergedBorderedRef, mergedBordered: mergedBorderedRef,
showPasswordToggle: computed(() => {
return props.showPasswordToggle && !props.disabled
}),
// methods // methods
handleCompositionStart, handleCompositionStart,
handleCompositionEnd, handleCompositionEnd,
@ -655,6 +672,9 @@ export default defineComponent({
handleChange, handleChange,
handleClick, handleClick,
handleClear, handleClear,
handlePasswordToggleClick,
handlePasswordToggleMousedown,
handlePasswordToggleMouseup,
handleWrapperKeyDown, handleWrapperKeyDown,
handleTextAreaMirrorResize, handleTextAreaMirrorResize,
mergedTheme: themeRef, mergedTheme: themeRef,
@ -852,7 +872,13 @@ export default defineComponent({
<div class={`${mergedClsPrefix}-input__input`}> <div class={`${mergedClsPrefix}-input__input`}>
<input <input
ref="inputElRef" ref="inputElRef"
type={this.type} type={
this.type === 'password' &&
this.showPasswordToggle &&
this.passwordVisible
? 'text'
: this.type
}
class={`${mergedClsPrefix}-input__input-el`} class={`${mergedClsPrefix}-input__input-el`}
tabindex={ tabindex={
this.passivelyActivated && !this.activated ? -1 : undefined this.passivelyActivated && !this.activated ? -1 : undefined
@ -892,7 +918,10 @@ export default defineComponent({
</div> </div>
)} )}
{!this.pair && {!this.pair &&
(this.$slots.suffix || this.clearable || this.showCount) ? ( (this.$slots.suffix ||
this.clearable ||
this.showCount ||
this.showPasswordToggle) ? (
<div class={`${mergedClsPrefix}-input__suffix`}> <div class={`${mergedClsPrefix}-input__suffix`}>
{[ {[
renderSlot(this.$slots, 'suffix'), renderSlot(this.$slots, 'suffix'),
@ -907,6 +936,20 @@ export default defineComponent({
) : null, ) : null,
this.showCount && this.type !== 'textarea' ? ( this.showCount && this.type !== 'textarea' ? (
<WordCount /> <WordCount />
) : null,
this.showPasswordToggle && this.type === 'password' ? (
<NBaseIcon
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-input__eye`}
onMousedown={this.handlePasswordToggleMousedown}
onMouseup={this.handlePasswordToggleMouseup}
onClick={this.handlePasswordToggleClick}
>
{{
default: () =>
this.passwordVisible ? <EyeIcon /> : <EyeOffIcon />
}}
</NBaseIcon>
) : null ) : null
]} ]}
</div> </div>

View File

@ -283,9 +283,13 @@ export default c([
cE('prefix', { cE('prefix', {
marginRight: '4px' marginRight: '4px'
}), }),
cE('suffix', { cE('suffix', `
marginLeft: '4px' margin-left: 4px;
}), `, [
cE('eye', `
cursor: pointer;
`)
]),
cE('suffix, prefix', ` cE('suffix, prefix', `
transition: color .3s var(--bezier); transition: color .3s var(--bezier);
flex-wrap: nowrap; flex-wrap: nowrap;