diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd1e9f50fbc..1f63a40be8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
- Added possibility to hide "Rows per page" select in `EuiDataGrid` ([#3700](https://github.com/elastic/eui/pull/3700))
- Updated lodash to `v4.17.19` ([#3764](https://github.com/elastic/eui/pull/3764))
- Added `returnKey` glyph to `EuiIcon` ([#3783](https://github.com/elastic/eui/pull/3783))
+- Added `type` prop to `EuiFieldPassword` to support toggling of obfuscation ([#3751](https://github.com/elastic/eui/pull/3751))
**Bug fixes**
diff --git a/src-docs/src/views/datagrid/styling.js b/src-docs/src/views/datagrid/styling.js
index 982535a6ad4..c719acf0948 100644
--- a/src-docs/src/views/datagrid/styling.js
+++ b/src-docs/src/views/datagrid/styling.js
@@ -428,7 +428,6 @@ export default class DataGrid extends Component {
button={styleButton}
isOpen={this.state.isPopoverOpen}
anchorPosition="rightUp"
- zIndex={3}
closePopover={this.closePopover.bind(this)}>
@@ -505,7 +504,6 @@ export default class DataGrid extends Component {
button={toolbarButton}
isOpen={this.state.isToolbarPopoverOpen}
anchorPosition="rightUp"
- zIndex={3}
closePopover={this.closeToolbarPopover.bind(this)}>
+
+`;
+
+exports[`EuiFieldPassword props compressed is rendered 1`] = `
+
+`;
+
+exports[`EuiFieldPassword props dual type also renders append 1`] = `
+
`;
-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.scss b/src/components/form/field_password/_field_password.scss
index 00f724ea2b3..17ea2e5277b 100644
--- a/src/components/form/field_password/_field_password.scss
+++ b/src/components/form/field_password/_field_password.scss
@@ -7,3 +7,9 @@
@include euiFormControlWithIcon($isIconOptional: false, $side: 'left', $compressed: true);
}
}
+
+// sass-lint:disable-block no-vendor-prefixes
+// Only remove Edge's inernal reveal button if we're providing a custom one
+.euiFieldPassword--withToggle::-ms-reveal {
+ display: none;
+}
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 9408ecfb7fa..6d1c02edb7e 100644
--- a/src/components/form/field_password/field_password.tsx
+++ b/src/components/form/field_password/field_password.tsx
@@ -17,7 +17,12 @@
* under the License.
*/
-import React, { InputHTMLAttributes, Ref, FunctionComponent } from 'react';
+import React, {
+ InputHTMLAttributes,
+ FunctionComponent,
+ useState,
+ Ref,
+} from 'react';
import { CommonProps } from '../../common';
import classNames from 'classnames';
@@ -27,6 +32,9 @@ import {
} from '../form_control_layout';
import { EuiValidatableControl } from '../validatable_control';
+import { EuiButtonIcon, EuiButtonIconProps } from '../../button';
+import { useEuiI18n } from '../../i18n';
+import { useCombinedRefs } from '../../../services';
export type EuiFieldPasswordProps = InputHTMLAttributes &
CommonProps & {
@@ -47,6 +55,18 @@ export type EuiFieldPasswordProps = InputHTMLAttributes &
* `string` | `ReactElement` or an array of these
*/
append?: EuiFormControlLayoutProps['append'];
+
+ /**
+ * Change the `type` of input for manually handling obfuscation.
+ * The `dual` option adds the ability to toggle the obfuscation of the input by
+ * adding an icon button as the first `append` element
+ */
+ type?: 'password' | 'text' | 'dual';
+
+ /**
+ * Additional props to apply to the dual toggle. Extends EuiButtonIcon
+ */
+ dualToggleProps?: EuiButtonIconProps;
};
export const EuiFieldPassword: FunctionComponent = ({
@@ -59,18 +79,70 @@ export const EuiFieldPassword: FunctionComponent = ({
fullWidth,
isLoading,
compressed,
- inputRef,
+ inputRef: _inputRef,
prepend,
append,
+ type = 'password',
+ dualToggleProps,
...rest
}) => {
+ // Set the initial input type to `password` if they want dual
+ const [inputType, setInputType] = useState(
+ type === 'dual' ? 'password' : type
+ );
+
+ // Setup toggle aria-label
+ const [showPasswordLabel, maskPasswordLabel] = useEuiI18n(
+ ['euiFieldPassword.showPassword', 'euiFieldPassword.maskPassword'],
+ [
+ 'Show password as plain text. Note: this will visually expose your password on the screen.',
+ 'Mask password',
+ ]
+ );
+
+ // Setup the inputRef to auto-focus when toggling visibility
+ const [inputRef, _setInputRef] = useState(null);
+ const setInputRef = useCombinedRefs([_setInputRef, _inputRef]);
+
+ const handleToggle = (isVisible: boolean) => {
+ setInputType(isVisible ? 'password' : 'text');
+ if (inputRef) {
+ inputRef.focus();
+ }
+ };
+
+ // 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);
+ // 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 = (
+ handleToggle(isVisible)}
+ aria-label={isVisible ? maskPasswordLabel : showPasswordLabel}
+ title={isVisible ? maskPasswordLabel : showPasswordLabel}
+ disabled={rest.disabled}
+ />
+ );
+ 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 || append,
+ 'euiFieldPassword--inGroup': prepend || finalAppend,
+ 'euiFieldPassword--withToggle': type === 'dual',
},
className
);
@@ -82,16 +154,16 @@ export const EuiFieldPassword: FunctionComponent = ({
isLoading={isLoading}
compressed={compressed}
prepend={prepend}
- append={append}>
+ append={finalAppend}>
diff --git a/src/services/hooks/index.ts b/src/services/hooks/index.ts
index 94741454012..7a7b08a4b42 100644
--- a/src/services/hooks/index.ts
+++ b/src/services/hooks/index.ts
@@ -17,4 +17,5 @@
* under the License.
*/
+export * from './useCombinedRefs';
export * from './useDependentState';
diff --git a/src/services/hooks/useCombinedRefs.ts b/src/services/hooks/useCombinedRefs.ts
new file mode 100644
index 00000000000..244df11e92a
--- /dev/null
+++ b/src/services/hooks/useCombinedRefs.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { MutableRefObject, Ref, useCallback } from 'react';
+
+/*
+ * For use when a component needs to set `ref` objects from multiple sources.
+ * For instance, if a component accepts a `ref` prop but also needs its own
+ * local reference for calculations, etc.
+ * This hook handles setting multiple `ref`s of any available `ref` type
+ * in a single callback function.
+ */
+export const useCombinedRefs = (
+ refs: Array[ | MutableRefObject | undefined>
+) => {
+ return useCallback(
+ (node: T) =>
+ refs.forEach(ref => {
+ if (!ref) return;
+
+ if (typeof ref === 'function') {
+ ref(node);
+ } else {
+ (ref as MutableRefObject).current = node;
+ }
+ }),
+ [refs]
+ );
+};
diff --git a/src/services/index.ts b/src/services/index.ts
index 7409ce75601..4ca4aef8ab3 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -108,4 +108,4 @@ export {
export { EuiWindowEvent } from './window_event';
-export { useDependentState } from './hooks';
+export { useCombinedRefs, useDependentState } from './hooks';
]