From 6ee61f6f0b7a373ea5f46fc15d53442656c124bc Mon Sep 17 00:00:00 2001 From: He Linming Date: Mon, 31 Oct 2022 21:15:59 +0800 Subject: [PATCH] feat(Input.Password): password visibility controllable (#38216) * feat: support controllable visible of password * test: add some unit test for Password * refactor: visibilityToggle API * test: update snapshots * Update components/input/Password.tsx Co-authored-by: afc163 --- components/input/Password.tsx | 31 +++++++-- components/input/__tests__/Password.test.tsx | 22 +++++++ .../__snapshots__/demo-extend.test.ts.snap | 64 +++++++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 64 +++++++++++++++++++ components/input/demo/password-input.md | 33 +++++++--- components/input/index.en-US.md | 9 ++- components/input/index.zh-CN.md | 9 ++- 7 files changed, 215 insertions(+), 17 deletions(-) diff --git a/components/input/Password.tsx b/components/input/Password.tsx index d205b6e9a6..1f96e6cbb7 100644 --- a/components/input/Password.tsx +++ b/components/input/Password.tsx @@ -14,10 +14,15 @@ import Input from './Input'; const defaultIconRender = (visible: boolean) => visible ? : ; +type VisibilityToggle = { + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; +}; + export interface PasswordProps extends InputProps { readonly inputPrefixCls?: string; readonly action?: string; - visibilityToggle?: boolean; + visibilityToggle?: boolean | VisibilityToggle; iconRender?: (visible: boolean) => React.ReactNode; } @@ -27,9 +32,20 @@ const ActionMap: Record = { }; const Password = React.forwardRef((props, ref) => { - const [visible, setVisible] = useState(false); + const { visibilityToggle = true } = props; + const visibilityControlled = + typeof visibilityToggle === 'object' && visibilityToggle.visible !== undefined; + const [visible, setVisible] = useState(() => + visibilityControlled ? visibilityToggle.visible! : false, + ); const inputRef = useRef(null); + React.useEffect(() => { + if (visibilityControlled) { + setVisible(visibilityToggle.visible!); + } + }, [visibilityControlled, visibilityToggle]); + // Remove Password value const removePasswordTimeout = useRemovePasswordTimeout(inputRef); @@ -41,7 +57,13 @@ const Password = React.forwardRef((props, ref) => { if (visible) { removePasswordTimeout(); } - setVisible(prevState => !prevState); + setVisible(prevState => { + const newState = !prevState; + if (typeof visibilityToggle === 'object') { + visibilityToggle.onVisibleChange?.(newState); + } + return newState; + }); }; const getIcon = (prefixCls: string) => { @@ -72,7 +94,6 @@ const Password = React.forwardRef((props, ref) => { prefixCls: customizePrefixCls, inputPrefixCls: customizeInputPrefixCls, size, - visibilityToggle = true, ...restProps } = props; @@ -85,7 +106,7 @@ const Password = React.forwardRef((props, ref) => { }); const omittedProps: InputProps = { - ...omit(restProps, ['suffix', 'iconRender']), + ...omit(restProps, ['suffix', 'iconRender', 'visibilityToggle']), type: visible ? 'text' : 'password', className: inputClassName, prefixCls: inputPrefixCls, diff --git a/components/input/__tests__/Password.test.tsx b/components/input/__tests__/Password.test.tsx index e6c1a74e96..fe3a987a5c 100644 --- a/components/input/__tests__/Password.test.tsx +++ b/components/input/__tests__/Password.test.tsx @@ -121,4 +121,26 @@ describe('Input.Password', () => { await sleep(); expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy(); }); + + it('should control password visible', () => { + const { container, rerender } = render(); + expect(container.querySelectorAll('.anticon-eye').length).toBe(1); + rerender(); + expect(container.querySelectorAll('.anticon-eye-invisible').length).toBe(1); + }); + + it('should call onPasswordVisibleChange when visible is changed', () => { + const handlePasswordVisibleChange = jest.fn(); + const { container, rerender } = render( + , + ); + fireEvent.click(container.querySelector('.ant-input-password-icon')!); + expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(1); + rerender( + , + ); + expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(1); + fireEvent.click(container.querySelector('.ant-input-password-icon')!); + expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(2); + }); }); diff --git a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap index cc00e93802..6f6ea67cf1 100644 --- a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -8869,6 +8869,7 @@ exports[`renders ./components/input/demo/password-input.md extend context correc
+
+
+
+ + + + + + + + +
+
+ +
+
+
`; diff --git a/components/input/__tests__/__snapshots__/demo.test.ts.snap b/components/input/__tests__/__snapshots__/demo.test.ts.snap index 6462706fe7..64d68483fa 100644 --- a/components/input/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/input/__tests__/__snapshots__/demo.test.ts.snap @@ -2667,6 +2667,7 @@ exports[`renders ./components/input/demo/password-input.md correctly 1`] = `
+
+
+
+ + + + + + + + +
+
+ +
+
+
`; diff --git a/components/input/demo/password-input.md b/components/input/demo/password-input.md index 2208600ef9..0e2fd8e09f 100644 --- a/components/input/demo/password-input.md +++ b/components/input/demo/password-input.md @@ -15,18 +15,31 @@ Input type of password. ```tsx import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons'; -import { Input, Space } from 'antd'; +import { Button, Input, Space } from 'antd'; import React from 'react'; -const App: React.FC = () => ( - - - (visible ? : )} - /> - -); +const App: React.FC = () => { + const [passwordVisible, setPasswordVisible] = React.useState(false); + + return ( + + + (visible ? : )} + /> + + + + + + ); +}; export default App; ``` diff --git a/components/input/index.en-US.md b/components/input/index.en-US.md index 45828b5a6d..d273d44bc7 100644 --- a/components/input/index.en-US.md +++ b/components/input/index.en-US.md @@ -85,7 +85,14 @@ Supports all props of `Input`. | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | | iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 | -| visibilityToggle | Whether show toggle button | boolean | true | | +| visibilityToggle | Whether show toggle button or control password visible | boolean \| [VisibilityToggle](#VisibilityToggle) | true | | + +#### VisibilityToggle + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| visible | Whether the password is show or hide | boolean | false | 4.24.0 | +| onVisibleChange | Callback executed when visibility of the password is changed | boolean | - | 4.24.0 | #### Input Methods diff --git a/components/input/index.zh-CN.md b/components/input/index.zh-CN.md index a51f6dc8e1..d96444e2fe 100644 --- a/components/input/index.zh-CN.md +++ b/components/input/index.zh-CN.md @@ -86,7 +86,14 @@ Input 的其他属性和 React 自带的 [input](https://reactjs.org/docs/dom-el | 参数 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 | -| visibilityToggle | 是否显示切换按钮 | boolean | true | | +| visibilityToggle | 是否显示切换按钮或者控制密码显隐 | boolean \| [VisibilityToggle](#VisibilityToggle) | true | | + +#### VisibilityToggle + +| Property | Description | Type | Default | Version | +| --------------- | -------------------- | ------- | ------- | ------- | +| visible | 用于手动控制密码显隐 | boolean | false | 4.24 | +| onVisibleChange | 显隐密码的回调 | boolean | - | 4.24 | #### Input Methods