diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md
index 810f6370ce3d56..8f6787e3922baa 100644
--- a/packages/dataviews/CHANGELOG.md
+++ b/packages/dataviews/CHANGELOG.md
@@ -12,6 +12,7 @@
- Dataform: Add new `telephone` field type and field control. [#71498](https://github.com/WordPress/gutenberg/pull/71498)
- DataForm: introduce a new `row` layout, check the README for details. [#71124](https://github.com/WordPress/gutenberg/pull/71124)
- Dataform: Add new `url` field type and field control. [#71518](https://github.com/WordPress/gutenberg/pull/71518)
+- Dataform: Add new `password` field type and field control. [#71545](https://github.com/WordPress/gutenberg/pull/71545)
## 8.0.0 (2025-09-03)
diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx
index 3eaab67f7ca94b..48d58314dd3aaa 100644
--- a/packages/dataviews/src/components/dataform/stories/index.story.tsx
+++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx
@@ -395,6 +395,7 @@ const ValidationComponent = ( {
integer: number;
boolean: boolean;
customEdit: string;
+ password: string;
};
const [ post, setPost ] = useState< ValidatedItem >( {
@@ -406,6 +407,7 @@ const ValidationComponent = ( {
integer: 2,
boolean: true,
customEdit: 'custom control',
+ password: 'secretpassword123',
} );
const customTextRule = ( value: ValidatedItem ) => {
@@ -451,6 +453,20 @@ const ValidationComponent = ( {
return null;
};
+ const customPasswordRule = ( value: ValidatedItem ) => {
+ if ( value.password.length < 8 ) {
+ return 'Password must be at least 8 characters long.';
+ }
+ if ( ! /[A-Z]/.test( value.password ) ) {
+ return 'Password must contain at least one uppercase letter.';
+ }
+ if ( ! /[0-9]/.test( value.password ) ) {
+ return 'Password must contain at least one number.';
+ }
+
+ return null;
+ };
+
const maybeCustomRule = (
rule: ( item: ValidatedItem ) => null | string
) => {
@@ -528,6 +544,15 @@ const ValidationComponent = ( {
required,
},
},
+ {
+ id: 'password',
+ type: 'password',
+ label: 'Password',
+ isValid: {
+ required,
+ custom: maybeCustomRule( customPasswordRule ),
+ },
+ },
];
const form = {
@@ -541,6 +566,7 @@ const ValidationComponent = ( {
'integer',
'boolean',
'customEdit',
+ 'password',
],
};
diff --git a/packages/dataviews/src/dataform-controls/email.tsx b/packages/dataviews/src/dataform-controls/email.tsx
index 18fe4d37258de4..6c383196a09d32 100644
--- a/packages/dataviews/src/dataform-controls/email.tsx
+++ b/packages/dataviews/src/dataform-controls/email.tsx
@@ -7,7 +7,7 @@ import { atSymbol } from '@wordpress/icons';
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
-import ValidatedText from './utils/validated-text';
+import ValidatedText from './utils/validated-input';
export default function Email< Item >( {
data,
diff --git a/packages/dataviews/src/dataform-controls/index.tsx b/packages/dataviews/src/dataform-controls/index.tsx
index 899174faa97ffe..ed50be78433e31 100644
--- a/packages/dataviews/src/dataform-controls/index.tsx
+++ b/packages/dataviews/src/dataform-controls/index.tsx
@@ -25,6 +25,7 @@ import toggle from './toggle';
import toggleGroup from './toggle-group';
import array from './array';
import color from './color';
+import password from './password';
interface FormControls {
[ key: string ]: ComponentType< DataFormControlProps< any > >;
@@ -40,6 +41,7 @@ const FORM_CONTROLS: FormControls = {
telephone,
url,
integer,
+ password,
radio,
select,
text,
diff --git a/packages/dataviews/src/dataform-controls/password.tsx b/packages/dataviews/src/dataform-controls/password.tsx
new file mode 100644
index 00000000000000..926c492a459fca
--- /dev/null
+++ b/packages/dataviews/src/dataform-controls/password.tsx
@@ -0,0 +1,50 @@
+/**
+ * WordPress dependencies
+ */
+import { Button } from '@wordpress/components';
+import { useCallback, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { seen, unseen } from '@wordpress/icons';
+/**
+ * Internal dependencies
+ */
+import ValidatedText from './utils/validated-input';
+import type { DataFormControlProps } from '../types';
+
+export default function Password< Item >( {
+ data,
+ field,
+ onChange,
+ hideLabelFromVision,
+}: DataFormControlProps< Item > ) {
+ const [ isVisible, setIsVisible ] = useState( false );
+
+ const toggleVisibility = useCallback( () => {
+ setIsVisible( ( prev ) => ! prev );
+ }, [] );
+
+ return (
+
+ ),
+ } }
+ />
+ );
+}
diff --git a/packages/dataviews/src/dataform-controls/telephone.tsx b/packages/dataviews/src/dataform-controls/telephone.tsx
index 882d20809a03de..fdb6741e8f9424 100644
--- a/packages/dataviews/src/dataform-controls/telephone.tsx
+++ b/packages/dataviews/src/dataform-controls/telephone.tsx
@@ -7,7 +7,7 @@ import { mobile } from '@wordpress/icons';
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
-import ValidatedText from './utils/validated-text';
+import ValidatedText from './utils/validated-input';
export default function Telephone< Item >( {
data,
diff --git a/packages/dataviews/src/dataform-controls/text.tsx b/packages/dataviews/src/dataform-controls/text.tsx
index 65991ced00cf08..b253789c4d27ea 100644
--- a/packages/dataviews/src/dataform-controls/text.tsx
+++ b/packages/dataviews/src/dataform-controls/text.tsx
@@ -2,7 +2,7 @@
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
-import ValidatedText from './utils/validated-text';
+import ValidatedText from './utils/validated-input';
export default function Text< Item >( {
data,
diff --git a/packages/dataviews/src/dataform-controls/url.tsx b/packages/dataviews/src/dataform-controls/url.tsx
index 616f369f7da937..6a3206c0fb4463 100644
--- a/packages/dataviews/src/dataform-controls/url.tsx
+++ b/packages/dataviews/src/dataform-controls/url.tsx
@@ -7,7 +7,7 @@ import { link } from '@wordpress/icons';
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
-import ValidatedText from './utils/validated-text';
+import ValidatedText from './utils/validated-input';
export default function Url< Item >( {
data,
diff --git a/packages/dataviews/src/dataform-controls/utils/validated-text.tsx b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx
similarity index 84%
rename from packages/dataviews/src/dataform-controls/utils/validated-text.tsx
rename to packages/dataviews/src/dataform-controls/utils/validated-input.tsx
index 7fdab124ce2cfb..78db2db16cdd45 100644
--- a/packages/dataviews/src/dataform-controls/utils/validated-text.tsx
+++ b/packages/dataviews/src/dataform-controls/utils/validated-input.tsx
@@ -5,6 +5,7 @@ import {
Icon,
privateApis,
__experimentalInputControlPrefixWrapper as InputControlPrefixWrapper,
+ __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper,
} from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
@@ -21,11 +22,15 @@ export type DataFormValidatedTextControlProps< Item > =
/**
* The input type of the control.
*/
- type?: 'text' | 'email' | 'tel' | 'url';
+ type?: 'text' | 'email' | 'tel' | 'url' | 'password';
/**
* Optional icon to display as prefix.
*/
icon?: React.ComponentType | React.ReactElement;
+ /**
+ * Optional icon to display as suffix.
+ */
+ suffix?: React.ReactElement;
};
export default function ValidatedText< Item >( {
@@ -35,6 +40,7 @@ export default function ValidatedText< Item >( {
hideLabelFromVision,
type,
icon,
+ suffix,
}: DataFormValidatedTextControlProps< Item > ) {
const { id, label, placeholder, description } = field;
const value = field.getValue( { item: data } );
@@ -90,6 +96,13 @@ export default function ValidatedText< Item >( {
) : undefined
}
+ suffix={
+ suffix ? (
+
+ { suffix }
+
+ ) : undefined
+ }
__next40pxDefaultSize
/>
);
diff --git a/packages/dataviews/src/field-types/index.tsx b/packages/dataviews/src/field-types/index.tsx
index eff981ca57d2d1..8bfff0adbe2db8 100644
--- a/packages/dataviews/src/field-types/index.tsx
+++ b/packages/dataviews/src/field-types/index.tsx
@@ -21,6 +21,7 @@ import { default as date } from './date';
import { default as boolean } from './boolean';
import { default as media } from './media';
import { default as array } from './array';
+import { default as password } from './password';
import { default as telephone } from './telephone';
import { default as color } from './color';
import { default as url } from './url';
@@ -68,6 +69,10 @@ export default function getFieldTypeDefinition< Item >(
return array;
}
+ if ( 'password' === type ) {
+ return password;
+ }
+
if ( 'telephone' === type ) {
return telephone;
}
diff --git a/packages/dataviews/src/field-types/password.tsx b/packages/dataviews/src/field-types/password.tsx
new file mode 100644
index 00000000000000..b5c12a12d39865
--- /dev/null
+++ b/packages/dataviews/src/field-types/password.tsx
@@ -0,0 +1,46 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import type {
+ DataViewRenderFieldProps,
+ SortDirection,
+ NormalizedField,
+ FieldTypeDefinition,
+} from '../types';
+import { renderFromElements } from '../utils';
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function sort( valueA: any, valueB: any, direction: SortDirection ) {
+ // Passwords should not be sortable for security reasons
+ return 0;
+}
+
+export default {
+ sort,
+ isValid: {
+ custom: ( item: any, field: NormalizedField< any > ) => {
+ const value = field.getValue( { item } );
+ if ( field?.elements ) {
+ const validValues = field.elements.map( ( f ) => f.value );
+ if ( ! validValues.includes( value ) ) {
+ return __( 'Value must be one of the elements.' );
+ }
+ }
+
+ return null;
+ },
+ },
+ Edit: 'password',
+ render: ( { item, field }: DataViewRenderFieldProps< any > ) => {
+ return field.elements
+ ? renderFromElements( { item, field } )
+ : '••••••••';
+ },
+ enableSorting: false,
+ filterBy: false,
+} satisfies FieldTypeDefinition< any >;
diff --git a/packages/dataviews/src/field-types/stories/index.story.tsx b/packages/dataviews/src/field-types/stories/index.story.tsx
index 4c0bad990db1e3..ba5927927c1c22 100644
--- a/packages/dataviews/src/field-types/stories/index.story.tsx
+++ b/packages/dataviews/src/field-types/stories/index.story.tsx
@@ -38,6 +38,7 @@ const meta = {
'datetime',
'email',
'integer',
+ 'password',
'radio',
'select',
'telephone',
@@ -76,6 +77,8 @@ type DataType = {
colorWithElements: string;
url: string;
urlWithElements: string;
+ password: string;
+ passwordWithElements: string;
media: string;
mediaWithElements: string;
array: string[];
@@ -106,6 +109,8 @@ const data: DataType[] = [
colorWithElements: 'rgba(255, 165, 0, 0.8)',
url: 'https://example.com',
urlWithElements: 'https://example.com',
+ password: 'secretpassword123',
+ passwordWithElements: 'secretpassword123',
media: 'https://live.staticflickr.com/7398/9458193857_e1256123e3_z.jpg',
mediaWithElements:
'https://live.staticflickr.com/7398/9458193857_e1256123e3_z.jpg',
@@ -292,6 +297,23 @@ const fields: Field< DataType >[] = [
},
],
},
+ {
+ id: 'password',
+ type: 'password',
+ label: 'Password',
+ description: 'Help for password.',
+ },
+ {
+ id: 'passwordWithElements',
+ type: 'password',
+ label: 'Password (with elements)',
+ description: 'Help for password with elements.',
+ elements: [
+ { value: 'secretpassword123', label: 'Secret Password' },
+ { value: 'adminpass456', label: 'Admin Password' },
+ { value: 'userpass789', label: 'User Password' },
+ ],
+ },
{
id: 'media',
type: 'media',
@@ -376,6 +398,7 @@ type ControlTypes =
| 'datetime'
| 'email'
| 'integer'
+ | 'password'
| 'radio'
| 'select'
| 'telephone'
@@ -701,6 +724,23 @@ export const Array = ( {
);
};
+export const Password = ( {
+ type,
+ Edit,
+}: {
+ type: PanelTypes;
+ Edit: ControlTypes;
+} ) => {
+ const passwordFields = useMemo(
+ () => fields.filter( ( field ) => field.type === 'password' ),
+ []
+ );
+
+ return (
+
+ );
+};
+
export const NoType = ( {
type,
Edit,
diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts
index bdec875cc6377c..2908c689ab9e79 100644
--- a/packages/dataviews/src/types.ts
+++ b/packages/dataviews/src/types.ts
@@ -103,6 +103,7 @@ export type FieldType =
| 'media'
| 'boolean'
| 'email'
+ | 'password'
| 'telephone'
| 'color'
| 'url'
diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts
index d60e3481e5775a..5397c1c13b0bf8 100644
--- a/packages/dataviews/src/validation.ts
+++ b/packages/dataviews/src/validation.ts
@@ -35,6 +35,8 @@ export function isItemValid< Item >(
( field.type === 'url' && isEmptyNullOrUndefined( value ) ) ||
( field.type === 'telephone' &&
isEmptyNullOrUndefined( value ) ) ||
+ ( field.type === 'password' &&
+ isEmptyNullOrUndefined( value ) ) ||
( field.type === 'integer' &&
isEmptyNullOrUndefined( value ) ) ||
( field.type === undefined && isEmptyNullOrUndefined( value ) )