From 763063baed2c8b68f11bc2d2fe3009c385824749 Mon Sep 17 00:00:00 2001 From: cchaos Date: Wed, 15 Jul 2020 14:18:55 -0400 Subject: [PATCH 01/12] [EuiFieldPassword] Support toggling obfuscation - Added props `type` and `canToggleVisibility` --- .../src/views/form_controls/field_password.js | 48 +++++++++----- .../field_password.test.tsx.snap | 14 +++-- .../form/field_password/field_password.tsx | 62 +++++++++++++++++-- 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src-docs/src/views/form_controls/field_password.js b/src-docs/src/views/form_controls/field_password.js index ef150eef6fc..9d782be5267 100644 --- a/src-docs/src/views/form_controls/field_password.js +++ b/src-docs/src/views/form_controls/field_password.js @@ -1,24 +1,42 @@ import React, { useState } from 'react'; -import { EuiFieldPassword } from '../../../../src/components'; +import { + EuiFieldPassword, + EuiSpacer, + EuiFormRow, + EuiCode, +} from '../../../../src/components'; import { DisplayToggles } from './display_toggles'; export default function() { const [value, setValue] = useState(''); - - const onChange = e => { - setValue(e.target.value); - }; - + const [value2, setValue2] = useState(''); return ( - /* DisplayToggles wrapper for Docs only */ - - onChange(e)} - aria-label="Use aria labels when no actual label is in use" - /> - + <> + {/* DisplayToggles wrapper for Docs only */} + + setValue(e.target.value)} + aria-label="Use aria labels when no actual label is in use" + /> + + + + Allowing canToggleVisibility to toggle + obfuscation + + }> + setValue2(e.target.value)} + canToggleVisibility + /> + + ); } diff --git a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap index e9e19a0bd11..8286013ff0c 100644 --- a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap +++ b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap @@ -2,6 +2,7 @@ exports[`EuiFieldPassword is rendered 1`] = ` @@ -40,6 +42,7 @@ exports[`EuiFieldPassword props fullWidth is rendered 1`] = ` exports[`EuiFieldPassword props isInvalid is rendered 1`] = ` @@ -58,6 +61,7 @@ exports[`EuiFieldPassword props isInvalid is rendered 1`] = ` exports[`EuiFieldPassword props isLoading is rendered 1`] = ` @@ -74,7 +78,7 @@ exports[`EuiFieldPassword props isLoading is rendered 1`] = ` exports[`EuiFieldPassword props prepend and append is rendered 1`] = ` & CommonProps & { @@ -47,6 +54,19 @@ export type EuiFieldPasswordProps = InputHTMLAttributes & * `string` | `ReactElement` or an array of these */ append?: EuiFormControlLayoutProps['append']; + + /** + * Adds the ability to toggle the obfuscation of the input by changing the + * type from `password` to `text`. + * Adds the button as the first `append` element + */ + canToggleVisibility?: boolean; + + /** + * Change the `type` of input for manually handling obfuscation. + * For use with a custom visibility toggle + */ + type?: 'password' | 'text'; }; export const EuiFieldPassword: FunctionComponent = ({ @@ -62,15 +82,49 @@ export const EuiFieldPassword: FunctionComponent = ({ inputRef, prepend, append, + type = 'password', + canToggleVisibility = true, ...rest }) => { + const [inputType, setInputType] = useState(type); + + // Convert any `append` elements to an array so the visibility + // toggle can be added to it + const appends = Array.isArray(append) ? append : []; + if (append && !Array.isArray(append)) appends.push(append); + if (canToggleVisibility) { + const isVisible = inputType === 'text'; + + const visibilityToggle = ( + + {([showPassword, maskPassword]: string[]) => ( + setInputType(isVisible ? 'password' : 'text')} + aria-label={isVisible ? showPassword : maskPassword} + title={isVisible ? showPassword : maskPassword} + /> + )} + + ); + appends.push(visibilityToggle); + } + const classes = classNames( 'euiFieldPassword', { 'euiFieldPassword--fullWidth': fullWidth, 'euiFieldPassword--compressed': compressed, 'euiFieldPassword-isLoading': isLoading, - 'euiFieldPassword--inGroup': prepend || append, + 'euiFieldPassword--inGroup': prepend || appends, }, className ); @@ -82,10 +136,10 @@ export const EuiFieldPassword: FunctionComponent = ({ isLoading={isLoading} compressed={compressed} prepend={prepend} - append={append}> + append={appends}> Date: Wed, 15 Jul 2020 15:48:42 -0400 Subject: [PATCH 02/12] =?UTF-8?q?Removed=20`canToggleVisibility`=20in=20fa?= =?UTF-8?q?vor=20of=20a=20third=20`type`=20called=20=E2=80=98dual=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/form_controls/field_password.js | 58 +-- .../form_controls/form_controls_example.js | 1 + .../field_password.test.tsx.snap | 450 +++++++++++++++--- .../field_password/field_password.test.tsx | 43 +- .../form/field_password/field_password.tsx | 38 +- 5 files changed, 467 insertions(+), 123 deletions(-) diff --git a/src-docs/src/views/form_controls/field_password.js b/src-docs/src/views/form_controls/field_password.js index 9d782be5267..95bf792e517 100644 --- a/src-docs/src/views/form_controls/field_password.js +++ b/src-docs/src/views/form_controls/field_password.js @@ -1,42 +1,34 @@ import React, { useState } from 'react'; -import { - EuiFieldPassword, - EuiSpacer, - EuiFormRow, - EuiCode, -} from '../../../../src/components'; +import { EuiFieldPassword, EuiSwitch } from '../../../../src/components'; import { DisplayToggles } from './display_toggles'; export default function() { const [value, setValue] = useState(''); - const [value2, setValue2] = useState(''); + const [dual, setDual] = useState(true); + return ( - <> - {/* DisplayToggles wrapper for Docs only */} - - setValue(e.target.value)} - aria-label="Use aria labels when no actual label is in use" - /> - - - - Allowing canToggleVisibility to toggle - obfuscation - - }> - setValue2(e.target.value)} - canToggleVisibility - /> - - + /* DisplayToggles wrapper for Docs only */ + { + setDual(e.target.checked); + }} + />, + ]}> + setValue(e.target.value)} + aria-label="Use aria labels when no actual label is in use" + /> + ); } diff --git a/src-docs/src/views/form_controls/form_controls_example.js b/src-docs/src/views/form_controls/form_controls_example.js index a533ffc8d02..d0f0aa27048 100644 --- a/src-docs/src/views/form_controls/form_controls_example.js +++ b/src-docs/src/views/form_controls/form_controls_example.js @@ -72,6 +72,7 @@ const fieldPasswordSnippet = [ placeholder="Placeholder text" value={value} onChange={onChange} + type="dual" />`, ]; diff --git a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap index 8286013ff0c..183bbdb0fed 100644 --- a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap +++ b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap @@ -1,95 +1,403 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiFieldPassword is rendered 1`] = ` - - - + + + +
+ + +
+ +`; + +exports[`EuiFieldPassword props compressed is rendered 1`] = ` +
+
+ + + +
+ + +
+
+`; + +exports[`EuiFieldPassword props dual type also renders append 1`] = ` +
+
+ + + +
+ + +
+ + + Span + + +
`; -exports[`EuiFieldPassword props fullWidth is rendered 1`] = ` - - - + + + +
+ + +
+ +
+`; + +exports[`EuiFieldPassword props fullWidth is rendered 1`] = ` +
+
+ + + +
+ + +
+
`; exports[`EuiFieldPassword props isInvalid is rendered 1`] = ` - - - - - + + + +
+ + +
+
`; exports[`EuiFieldPassword props isLoading is rendered 1`] = ` - - - - - +
+ + + +
+ + +
+ +
+
+
`; exports[`EuiFieldPassword props prepend and append is rendered 1`] = ` - + +
+ + + +
+ + +
+ +
+`; + +exports[`EuiFieldPassword props type dual is rendered 1`] = ` +
- - + + + +
+ + +
+ +
+`; + +exports[`EuiFieldPassword props type password is rendered 1`] = ` +
+
+ + + +
+ + +
+
+`; + +exports[`EuiFieldPassword props type text is rendered 1`] = ` +
+
+ + + +
+ + +
+
`; diff --git a/src/components/form/field_password/field_password.test.tsx b/src/components/form/field_password/field_password.test.tsx index 541b694a6c5..31bd3479f53 100644 --- a/src/components/form/field_password/field_password.test.tsx +++ b/src/components/form/field_password/field_password.test.tsx @@ -21,15 +21,18 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; -import { EuiFieldPassword } from './field_password'; +import { EuiFieldPassword, EuiFieldPasswordProps } from './field_password'; -jest.mock('../form_control_layout', () => ({ - EuiFormControlLayout: 'eui-form-control-layout', -})); jest.mock('../validatable_control', () => ({ EuiValidatableControl: 'eui-validatable-control', })); +const TYPES: Array = [ + 'password', + 'text', + 'dual', +]; + describe('EuiFieldPassword', () => { test('is rendered', () => { const component = render( @@ -72,5 +75,37 @@ describe('EuiFieldPassword', () => { expect(component).toMatchSnapshot(); }); + + test('compressed is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + describe('type', () => { + TYPES.forEach(type => { + test(`${type} is rendered`, () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('dualToggleProps is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('dual type also renders append', () => { + const component = render( + Span]} /> + ); + + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/components/form/field_password/field_password.tsx b/src/components/form/field_password/field_password.tsx index 89dffbd39f0..2ab1d165b54 100644 --- a/src/components/form/field_password/field_password.tsx +++ b/src/components/form/field_password/field_password.tsx @@ -32,7 +32,7 @@ import { } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; -import { EuiButtonIcon } from '../../button'; +import { EuiButtonIcon, EuiButtonIconProps } from '../../button'; import { EuiI18n } from '../../i18n'; export type EuiFieldPasswordProps = InputHTMLAttributes & @@ -56,17 +56,16 @@ export type EuiFieldPasswordProps = InputHTMLAttributes & append?: EuiFormControlLayoutProps['append']; /** - * Adds the ability to toggle the obfuscation of the input by changing the - * type from `password` to `text`. - * Adds the button as the first `append` element + * Change the `type` of input for manually handling obfuscation. + * The `dual` option adds the ability to toggle the obfuscation of the input by + * adding the an icon button as the first `append` element */ - canToggleVisibility?: boolean; + type?: 'password' | 'text' | 'dual'; /** - * Change the `type` of input for manually handling obfuscation. - * For use with a custom visibility toggle + * Additional props to apply to the dual toggle. Extends EuiButtonIcon */ - type?: 'password' | 'text'; + dualToggleProps?: EuiButtonIconProps; }; export const EuiFieldPassword: FunctionComponent = ({ @@ -83,16 +82,21 @@ export const EuiFieldPassword: FunctionComponent = ({ prepend, append, type = 'password', - canToggleVisibility = true, + dualToggleProps, ...rest }) => { - const [inputType, setInputType] = useState(type); + // Set the initial input type to `password` if they want dual + const [inputType, setInputType] = useState( + type === 'dual' ? 'password' : type + ); // Convert any `append` elements to an array so the visibility // toggle can be added to it const appends = Array.isArray(append) ? append : []; if (append && !Array.isArray(append)) appends.push(append); - if (canToggleVisibility) { + // Add a toggling button to switch between `password` and `input` if consumer wants `dual` + // https://www.w3schools.com/howto/howto_js_toggle_password.asp + if (type === 'dual') { const isVisible = inputType === 'text'; const visibilityToggle = ( @@ -107,10 +111,12 @@ export const EuiFieldPassword: FunctionComponent = ({ ]}> {([showPassword, maskPassword]: string[]) => ( setInputType(isVisible ? 'password' : 'text')} - aria-label={isVisible ? showPassword : maskPassword} - title={isVisible ? showPassword : maskPassword} + aria-label={isVisible ? maskPassword : showPassword} + title={isVisible ? maskPassword : showPassword} + disabled={rest.disabled} /> )} @@ -118,13 +124,15 @@ export const EuiFieldPassword: FunctionComponent = ({ appends.push(visibilityToggle); } + const finalAppend = appends.length ? appends : undefined; + const classes = classNames( 'euiFieldPassword', { 'euiFieldPassword--fullWidth': fullWidth, 'euiFieldPassword--compressed': compressed, 'euiFieldPassword-isLoading': isLoading, - 'euiFieldPassword--inGroup': prepend || appends, + 'euiFieldPassword--inGroup': prepend || finalAppend, }, className ); @@ -136,7 +144,7 @@ export const EuiFieldPassword: FunctionComponent = ({ isLoading={isLoading} compressed={compressed} prepend={prepend} - append={appends}> + append={finalAppend}> Date: Wed, 15 Jul 2020 16:47:22 -0400 Subject: [PATCH 03/12] Put focus back into input after toggling visibility --- .../form/field_password/field_password.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/form/field_password/field_password.tsx b/src/components/form/field_password/field_password.tsx index 2ab1d165b54..231724617b3 100644 --- a/src/components/form/field_password/field_password.tsx +++ b/src/components/form/field_password/field_password.tsx @@ -19,9 +19,9 @@ import React, { InputHTMLAttributes, - Ref, FunctionComponent, useState, + RefCallback, } from 'react'; import { CommonProps } from '../../common'; import classNames from 'classnames'; @@ -41,7 +41,7 @@ export type EuiFieldPasswordProps = InputHTMLAttributes & fullWidth?: boolean; isLoading?: boolean; compressed?: boolean; - inputRef?: Ref; + inputRef?: RefCallback; /** * Creates an input group with element(s) coming before input. @@ -90,6 +90,22 @@ export const EuiFieldPassword: FunctionComponent = ({ type === 'dual' ? 'password' : type ); + // Setup the inputRef to auto-focus when toggling visibility + const [input, setInput] = useState(null); + const inputRefCallback = (ref: HTMLInputElement | null) => { + setInput(ref); + if (inputRef) { + inputRef(ref); + } + }; + + const handleToggle = (isVisible: boolean) => { + setInputType(isVisible ? 'password' : 'text'); + if (input) { + input.focus(); + } + }; + // Convert any `append` elements to an array so the visibility // toggle can be added to it const appends = Array.isArray(append) ? append : []; @@ -113,7 +129,7 @@ export const EuiFieldPassword: FunctionComponent = ({ setInputType(isVisible ? 'password' : 'text')} + onClick={() => handleToggle(isVisible)} aria-label={isVisible ? maskPassword : showPassword} title={isVisible ? maskPassword : showPassword} disabled={rest.disabled} @@ -153,7 +169,7 @@ export const EuiFieldPassword: FunctionComponent = ({ placeholder={placeholder} className={classes} value={value} - ref={inputRef} + ref={inputRefCallback} {...rest} /> From cc6f2d7736ee88f522e20af5cfc7bf261baf5d92 Mon Sep 17 00:00:00 2001 From: cchaos Date: Wed, 15 Jul 2020 16:58:34 -0400 Subject: [PATCH 04/12] Back to using `Ref` instead of `RefCallback` --- .../form/field_password/field_password.tsx | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/form/field_password/field_password.tsx b/src/components/form/field_password/field_password.tsx index 231724617b3..c3a435f7034 100644 --- a/src/components/form/field_password/field_password.tsx +++ b/src/components/form/field_password/field_password.tsx @@ -21,7 +21,8 @@ import React, { InputHTMLAttributes, FunctionComponent, useState, - RefCallback, + Ref, + useCallback, } from 'react'; import { CommonProps } from '../../common'; import classNames from 'classnames'; @@ -41,7 +42,7 @@ export type EuiFieldPasswordProps = InputHTMLAttributes & fullWidth?: boolean; isLoading?: boolean; compressed?: boolean; - inputRef?: RefCallback; + inputRef?: Ref; /** * Creates an input group with element(s) coming before input. @@ -78,7 +79,7 @@ export const EuiFieldPassword: FunctionComponent = ({ fullWidth, isLoading, compressed, - inputRef, + inputRef: _inputRef, prepend, append, type = 'password', @@ -91,18 +92,24 @@ export const EuiFieldPassword: FunctionComponent = ({ ); // Setup the inputRef to auto-focus when toggling visibility - const [input, setInput] = useState(null); - const inputRefCallback = (ref: HTMLInputElement | null) => { - setInput(ref); - if (inputRef) { - inputRef(ref); - } - }; + const [inputRef, _setInputRef] = useState(null); + const setInputRef = useCallback( + (ref: HTMLInputElement | null) => { + _setInputRef(ref); + if (typeof _inputRef === 'function') { + _inputRef(ref); + } else if (_inputRef) { + // @ts-ignore need to mutate current + _inputRef.current = ref; + } + }, + [_inputRef] + ); const handleToggle = (isVisible: boolean) => { setInputType(isVisible ? 'password' : 'text'); - if (input) { - input.focus(); + if (inputRef) { + inputRef.focus(); } }; @@ -169,7 +176,7 @@ export const EuiFieldPassword: FunctionComponent = ({ placeholder={placeholder} className={classes} value={value} - ref={inputRefCallback} + ref={setInputRef} {...rest} /> From 5e64ef316c6a496baaa2f5374b1f7f3989344030 Mon Sep 17 00:00:00 2001 From: cchaos Date: Tue, 21 Jul 2020 11:23:18 -0400 Subject: [PATCH 05/12] Use new `useEuiI18n` method --- .../field_password.test.tsx.snap | 6 +-- .../form/field_password/field_password.tsx | 41 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap index 183bbdb0fed..8b5ad5d0e78 100644 --- a/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap +++ b/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap @@ -105,7 +105,7 @@ exports[`EuiFieldPassword props dual type also renders append 1`] = `