Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ const ValidationComponent = ( {
integer: number;
boolean: boolean;
customEdit: string;
password: string;
};

const [ post, setPost ] = useState< ValidatedItem >( {
Expand All @@ -406,6 +407,7 @@ const ValidationComponent = ( {
integer: 2,
boolean: true,
customEdit: 'custom control',
password: 'secretpassword123',
} );

const customTextRule = ( value: ValidatedItem ) => {
Expand Down Expand Up @@ -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
) => {
Expand Down Expand Up @@ -528,6 +544,15 @@ const ValidationComponent = ( {
required,
},
},
{
id: 'password',
type: 'password',
label: 'Password',
isValid: {
required,
custom: maybeCustomRule( customPasswordRule ),
},
},
];

const form = {
Expand All @@ -541,6 +566,7 @@ const ValidationComponent = ( {
'integer',
'boolean',
'customEdit',
'password',
],
};

Expand Down
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataform-controls/email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/dataviews/src/dataform-controls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 > >;
Expand All @@ -40,6 +41,7 @@ const FORM_CONTROLS: FormControls = {
telephone,
url,
integer,
password,
radio,
select,
text,
Expand Down
50 changes: 50 additions & 0 deletions packages/dataviews/src/dataform-controls/password.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ValidatedText
{ ...{
data,
field,
onChange,
hideLabelFromVision,
type: isVisible ? 'text' : 'password',
suffix: (
<Button
icon={ isVisible ? unseen : seen }
onClick={ toggleVisibility }
size="small"
variant="tertiary"
aria-label={
isVisible
? __( 'Hide password' )
: __( 'Show password' )
}
/>
),
} }
/>
);
}
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataform-controls/telephone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataform-controls/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataform-controls/url.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Icon,
privateApis,
__experimentalInputControlPrefixWrapper as InputControlPrefixWrapper,
__experimentalInputControlSuffixWrapper as InputControlSuffixWrapper,
} from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';

Expand All @@ -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 >( {
Expand All @@ -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 } );
Expand Down Expand Up @@ -90,6 +96,13 @@ export default function ValidatedText< Item >( {
</InputControlPrefixWrapper>
) : undefined
}
suffix={
suffix ? (
<InputControlSuffixWrapper variant="control">
{ suffix }
</InputControlSuffixWrapper>
) : undefined
}
__next40pxDefaultSize
/>
);
Expand Down
5 changes: 5 additions & 0 deletions packages/dataviews/src/field-types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +69,10 @@ export default function getFieldTypeDefinition< Item >(
return array;
}

if ( 'password' === type ) {
return password;
}

if ( 'telephone' === type ) {
return telephone;
}
Expand Down
46 changes: 46 additions & 0 deletions packages/dataviews/src/field-types/password.tsx
Original file line number Diff line number Diff line change
@@ -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 >;
40 changes: 40 additions & 0 deletions packages/dataviews/src/field-types/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const meta = {
'datetime',
'email',
'integer',
'password',
'radio',
'select',
'telephone',
Expand Down Expand Up @@ -76,6 +77,8 @@ type DataType = {
colorWithElements: string;
url: string;
urlWithElements: string;
password: string;
passwordWithElements: string;
media: string;
mediaWithElements: string;
array: string[];
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -376,6 +398,7 @@ type ControlTypes =
| 'datetime'
| 'email'
| 'integer'
| 'password'
| 'radio'
| 'select'
| 'telephone'
Expand Down Expand Up @@ -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 (
<FieldTypeStory fields={ passwordFields } type={ type } Edit={ Edit } />
);
};

export const NoType = ( {
type,
Edit,
Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export type FieldType =
| 'media'
| 'boolean'
| 'email'
| 'password'
| 'telephone'
| 'color'
| 'url'
Expand Down
2 changes: 2 additions & 0 deletions packages/dataviews/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) )
Expand Down
Loading