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 ) )