From 91cf536db46dc7a76471501623f496efe402508b Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 7 Jul 2023 09:38:49 +0200 Subject: [PATCH 1/8] add field validation component --- components/field-validation/index.js | 189 ++++++++++++++++++++++++++ components/field-validation/readme.md | 106 +++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 components/field-validation/index.js create mode 100644 components/field-validation/readme.md diff --git a/components/field-validation/index.js b/components/field-validation/index.js new file mode 100644 index 00000000..6d7c9363 --- /dev/null +++ b/components/field-validation/index.js @@ -0,0 +1,189 @@ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useMemo, forwardRef, cloneElement } from '@wordpress/element'; +import { dispatch } from '@wordpress/data'; +import { useFloating, autoUpdate } from '@floating-ui/react-dom'; +import { v4 as uuid } from 'uuid'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; + +/** + * Create Tests Array + * + * @description create a sanitized tests array. + * + * @param {boolean|string} required Field validation required prop. + * @param {boolean|Array} validate Field validation validate prop. + * + * @returns {Array} + */ +const createTestsArray = (required, validate) => { + const tests = []; + + if (required) { + const fn = (str) => typeof str === 'string' && str !== ''; + const res = typeof required === 'string' ? required : __('Required', '10up-block-library'); + tests.push([fn, res]); + } + + if (validate && Array.isArray(validate)) { + const isNested = (arr) => Array.isArray(arr) && arr.some((item) => Array.isArray(item)); + const sanitized = isNested(validate) ? validate : [validate]; + sanitized.map((entry) => tests.push(entry)); + } + + return tests; +}; + +/** + * Validate Tests Array + * + * @description test a value against the tests array. + * + * @param {string} value Field value. + * @param {Array} tests Tests array created from required and validate. + * + * @returns {boolean} responses + */ +const validateTestsArray = (value, tests) => { + const responses = []; + + tests.forEach((entry) => { + if (!Array.isArray(entry)) { + return; + } + + const [test, res] = entry; + const passes = test(value); + + if (passes === false) { + responses.push(res); + } + }); + + return responses; +}; + +/** + * Error Message + * + * @description style the validation error message. + * + * @returns + */ +const ErrorResponse = styled('div')` + --color-warning: #f00; + + color: var(--color-warning); +`; + +/** + * Error + * + * @description display validation error. + * + * @returns + */ +const Error = forwardRef((props, ref) => { + const { responses } = props; + + return ( + + {responses.map((response) => ( +
+ {response} +
+ ))} +
+ ); +}); + +/** + * Field Validation + * + * @description create new component which adds field validation abilities. + * + * @param {object} props Component props. + * @param {string} props.value Field validation value. + * @param {boolean|string} props.required Field validation required prop. + * @param {boolean|Array} props.validate Field validation validate array prop. + * @param {*} props.children Child components. + * + * @returns {HTMLElement} + */ +const FieldValidation = (props) => { + const { value, required = false, validate = false, children } = props; + + const tests = useMemo(() => createTestsArray(required, validate), [required, validate]); + + const { x, y, reference, floating, strategy } = useFloating({ + placement: 'bottom-start', + strategy: 'fixed', + whileElementsMounted: autoUpdate, + }); + + const [responses, setResponses] = useState(true); + const [lockId, setLockId] = useState(null); + + if (lockId === null) { + setLockId(uuid()); + } + + const validateTests = useMemo( + () => (value) => { + const test = value === '' && required ? [tests[0]] : tests; + return validateTestsArray(value, test); + }, + [required, tests], + ); + + const dispatchLock = useMemo( + () => (state) => { + if (state) { + dispatch('core/editor').lockPostSaving(lockId); + } else { + dispatch('core/editor').unlockPostSaving(lockId); + } + }, + [lockId], + ); + + useEffect(() => { + setResponses(validateTests(value)); + }, [value, validateTests]); + + useEffect(() => { + dispatchLock(responses.length > 0); + }, [responses, dispatchLock]); + + return ( + <> + {cloneElement(children, { ref: reference, ...children.props })} + + {responses.length > 0 && ( + + )} + + ); +}; + +export { FieldValidation }; + +FieldValidation.defaultProps = { + required: false, + validate: false, +}; + +FieldValidation.propTypes = { + value: PropTypes.string.isRequired, + required: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + validate: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), + children: PropTypes.node.isRequired, +}; diff --git a/components/field-validation/readme.md b/components/field-validation/readme.md new file mode 100644 index 00000000..d333e168 --- /dev/null +++ b/components/field-validation/readme.md @@ -0,0 +1,106 @@ +# Field Validation + +The Field Validation component enables the ability to add field validation. + +## Usage + +### Prop: Required + +`` provides a `required` prop which accepts true or a string. Passing true will display the default "Required" message, while passing a string enables the use of a custom message. + +```js +import { FieldValidation } from '@10up/block-components'; + +function BlockEdit(props) { + const { attributes, setAttributes } = props; + const { title } = attributes; + + return ( + <> + + setAttributes({ title: value })} + /> + + + + setAttributes({ title: value })} + /> + + + ) +} +``` + +### Prop: Validate + +`` can provide more extensive validation tests, `validate` accepts an array of tests. Each test entry is an array containing a callback function and messaging. + +```js +import { FieldValidation } from '@10up/block-components'; + +function BlockEdit(props) { + const { attributes, setAttributes } = props; + const { title } = attributes; + + return ( + <> + /^[a-zA-Z\s]+$/.test(value), __('Title requires a-z characters', '10up') ]} + > + setAttributes({ title: value })} + /> + + + ) +} +``` + +### Full example + +Required and validate props can be combined. The required validation will display if the value is empty, and validate will display if there is a value. + +```js +import { FieldValidation } from '@10up/block-components'; + +function BlockEdit(props) { + const { attributes, setAttributes } = props; + const { title } = attributes; + + return ( + <> + /^[a-zA-Z\s]+$/.test(value), __('Title requires a-z characters', '10up') ], + [ (value) => /^.{10,}$/.test(value), __('Title requires 10 characters or more', '10up') ], + ]} + > + setAttributes({ title: value })} + /> + + + ) +} +``` + +## Props + +| Name | Type | Default | Description | +| ---------- | ----------------- | -------- | -------------------------------------------------------------- | +| `value` | `string` | `undefined` | Value to validate | +| `required` | `boolean/string` | `Required` | Required validation message | +| `validate` | `array` | `[]` | Tests to validate value | From 41f65284ec9db57aac76880c11aecc1cb6a02be6 Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 7 Jul 2023 10:47:06 +0200 Subject: [PATCH 2/8] Minor readme cleanup --- components/field-validation/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/field-validation/readme.md b/components/field-validation/readme.md index d333e168..2332ad27 100644 --- a/components/field-validation/readme.md +++ b/components/field-validation/readme.md @@ -6,7 +6,7 @@ The Field Validation component enables the ability to add field validation. ### Prop: Required -`` provides a `required` prop which accepts true or a string. Passing true will display the default "Required" message, while passing a string enables the use of a custom message. +`` provides a `required` prop which accepts true or a string. Passing true will display the default response which is "Required", while passing a string enables the use of a custom response. ```js import { FieldValidation } from '@10up/block-components'; @@ -39,7 +39,7 @@ function BlockEdit(props) { ### Prop: Validate -`` can provide more extensive validation tests, `validate` accepts an array of tests. Each test entry is an array containing a callback function and messaging. +`` can provide more extensive validation tests, `validate` accepts an array of tests. Each test entry is an array containing a callback function and response. ```js import { FieldValidation } from '@10up/block-components'; @@ -102,5 +102,5 @@ function BlockEdit(props) { | Name | Type | Default | Description | | ---------- | ----------------- | -------- | -------------------------------------------------------------- | | `value` | `string` | `undefined` | Value to validate | -| `required` | `boolean/string` | `Required` | Required validation message | +| `required` | `boolean/string` | `Required` | Required validation response | | `validate` | `array` | `[]` | Tests to validate value | From a2bfbaf888d58b0dc16d004295172133ae0f6296 Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 7 Jul 2023 10:50:30 +0200 Subject: [PATCH 3/8] Minor readme cleanup --- components/field-validation/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/field-validation/readme.md b/components/field-validation/readme.md index 2332ad27..a8b35078 100644 --- a/components/field-validation/readme.md +++ b/components/field-validation/readme.md @@ -67,7 +67,7 @@ function BlockEdit(props) { ### Full example -Required and validate props can be combined. The required validation will display if the value is empty, and validate will display if there is a value. +Required and validate props can be combined. The required validation will display if the value is empty, and validate will display if there is a value available for custom tests. ```js import { FieldValidation } from '@10up/block-components'; @@ -80,7 +80,7 @@ function BlockEdit(props) { <> /^[a-zA-Z\s]+$/.test(value), __('Title requires a-z characters', '10up') ], [ (value) => /^.{10,}$/.test(value), __('Title requires 10 characters or more', '10up') ], From d73d6ea0af1c4860ccd899654c1aa5351697c9dd Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 7 Jul 2023 10:51:25 +0200 Subject: [PATCH 4/8] Minor readme cleanup --- components/field-validation/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/field-validation/readme.md b/components/field-validation/readme.md index a8b35078..e8f25629 100644 --- a/components/field-validation/readme.md +++ b/components/field-validation/readme.md @@ -102,5 +102,5 @@ function BlockEdit(props) { | Name | Type | Default | Description | | ---------- | ----------------- | -------- | -------------------------------------------------------------- | | `value` | `string` | `undefined` | Value to validate | -| `required` | `boolean/string` | `Required` | Required validation response | -| `validate` | `array` | `[]` | Tests to validate value | +| `required` | `boolean/string` | `false/Required` | Required validation response | +| `validate` | `array` | `false` | Tests to validate value | From 33f87b9a8d67794b459c2b18dd9839b10e933752 Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 28 Jul 2023 11:50:10 +0200 Subject: [PATCH 5/8] Implement field error classname --- components/field-validation/index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/components/field-validation/index.js b/components/field-validation/index.js index 6d7c9363..5986a0c9 100644 --- a/components/field-validation/index.js +++ b/components/field-validation/index.js @@ -3,6 +3,7 @@ import { useState, useEffect, useMemo, forwardRef, cloneElement } from '@wordpre import { dispatch } from '@wordpress/data'; import { useFloating, autoUpdate } from '@floating-ui/react-dom'; import { v4 as uuid } from 'uuid'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; @@ -21,7 +22,7 @@ const createTestsArray = (required, validate) => { if (required) { const fn = (str) => typeof str === 'string' && str !== ''; - const res = typeof required === 'string' ? required : __('Required', '10up-block-library'); + const res = typeof required === 'string' ? required : __('Required', '10up-block-components'); tests.push([fn, res]); } @@ -87,9 +88,9 @@ const Error = forwardRef((props, ref) => { const { responses } = props; return ( - + {responses.map((response) => ( -
+
{response}
))} @@ -155,9 +156,15 @@ const FieldValidation = (props) => { dispatchLock(responses.length > 0); }, [responses, dispatchLock]); + const fieldErorrClassName = responses.length > 0 ? 'tenup--block-components__validation-error__field' : ''; + return ( <> - {cloneElement(children, { ref: reference, ...children.props })} + {cloneElement(children, { + ref: reference, + ...children.props, + className: classnames(children.props?.className, fieldErorrClassName), + })} {responses.length > 0 && ( Date: Fri, 28 Jul 2023 12:08:59 +0200 Subject: [PATCH 6/8] Fix linting issues --- components/field-validation/index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/components/field-validation/index.js b/components/field-validation/index.js index 5986a0c9..8c52bd40 100644 --- a/components/field-validation/index.js +++ b/components/field-validation/index.js @@ -22,7 +22,8 @@ const createTestsArray = (required, validate) => { if (required) { const fn = (str) => typeof str === 'string' && str !== ''; - const res = typeof required === 'string' ? required : __('Required', '10up-block-components'); + const res = + typeof required === 'string' ? required : __('Required', '10up-block-components'); tests.push([fn, res]); } @@ -88,9 +89,16 @@ const Error = forwardRef((props, ref) => { const { responses } = props; return ( - + {responses.map((response) => ( -
+
{response}
))} @@ -156,14 +164,15 @@ const FieldValidation = (props) => { dispatchLock(responses.length > 0); }, [responses, dispatchLock]); - const fieldErorrClassName = responses.length > 0 ? 'tenup--block-components__validation-error__field' : ''; + const fieldErrorClassName = + responses.length > 0 ? 'tenup--block-components__validation-error__field' : ''; return ( <> {cloneElement(children, { ref: reference, ...children.props, - className: classnames(children.props?.className, fieldErorrClassName), + className: classnames(children.props?.className, fieldErrorClassName), })} {responses.length > 0 && ( From ab5a860284738a95f533bf123f470169dc2f7b0c Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 28 Jul 2023 12:17:43 +0200 Subject: [PATCH 7/8] Update field validation readme --- components/field-validation/readme.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/field-validation/readme.md b/components/field-validation/readme.md index e8f25629..489e59e8 100644 --- a/components/field-validation/readme.md +++ b/components/field-validation/readme.md @@ -97,6 +97,17 @@ function BlockEdit(props) { } ``` +## Styling + +The following elements/states are available for styling; + +- `.tenup--block-components__validation-error__field` + - State/class applied to the field when there is a validation error. +- `.tenup--block-components__validation-error__response` + - The responses container element. +- `.tenup--block-components__validation-error__response-rule` + - Each response rule element within the responses container. + ## Props | Name | Type | Default | Description | From a504f38f63ac90195cf3772a2ec277d67752394b Mon Sep 17 00:00:00 2001 From: Darren Jacoby Date: Fri, 28 Jul 2023 17:27:56 +0200 Subject: [PATCH 8/8] Implement basic styling --- components/field-validation/index.js | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/components/field-validation/index.js b/components/field-validation/index.js index 8c52bd40..5a9d83c7 100644 --- a/components/field-validation/index.js +++ b/components/field-validation/index.js @@ -6,6 +6,7 @@ import { v4 as uuid } from 'uuid'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; +import { Global, css } from '@emotion/react'; /** * Create Tests Array @@ -73,9 +74,11 @@ const validateTestsArray = (value, tests) => { * @returns */ const ErrorResponse = styled('div')` - --color-warning: #f00; - - color: var(--color-warning); + color: var(--wp--preset--color--vivid-red); + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, + Helvetica Neue, sans-serif; + font-size: 13px; + font-weight: 500; `; /** @@ -166,14 +169,26 @@ const FieldValidation = (props) => { const fieldErrorClassName = responses.length > 0 ? 'tenup--block-components__validation-error__field' : ''; + const mergedClassName = classnames(children.props?.className, fieldErrorClassName); + + const clonedChildren = cloneElement(children, { + ref: reference, + ...children.props, + className: mergedClassName, + }); return ( <> - {cloneElement(children, { - ref: reference, - ...children.props, - className: classnames(children.props?.className, fieldErrorClassName), - })} + + + {clonedChildren} {responses.length > 0 && (