@wordpress/dataviews 11.2.1-next.v.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/build/components/dataform-controls/combobox.cjs +80 -0
- package/build/components/dataform-controls/combobox.cjs.map +7 -0
- package/build/components/dataform-controls/date.cjs +35 -10
- package/build/components/dataform-controls/date.cjs.map +2 -2
- package/build/components/dataform-controls/index.cjs +2 -0
- package/build/components/dataform-controls/index.cjs.map +3 -3
- package/build/components/dataform-layouts/card/index.cjs +58 -3
- package/build/components/dataform-layouts/card/index.cjs.map +3 -3
- package/build/components/dataform-layouts/panel/dropdown.cjs +18 -8
- package/build/components/dataform-layouts/panel/dropdown.cjs.map +3 -3
- package/build/components/dataform-layouts/panel/index.cjs +5 -3
- package/build/components/dataform-layouts/panel/index.cjs.map +2 -2
- package/build/components/dataform-layouts/panel/modal.cjs +16 -10
- package/build/components/dataform-layouts/panel/modal.cjs.map +3 -3
- package/build/components/dataviews-bulk-actions/index.cjs +16 -18
- package/build/components/dataviews-bulk-actions/index.cjs.map +3 -3
- package/build/components/dataviews-item-actions/index.cjs +4 -1
- package/build/components/dataviews-item-actions/index.cjs.map +2 -2
- package/build/components/dataviews-layouts/activity/activity-item.cjs +6 -1
- package/build/components/dataviews-layouts/activity/activity-item.cjs.map +2 -2
- package/build/components/dataviews-layouts/table/column-header-menu.cjs +73 -66
- package/build/components/dataviews-layouts/table/column-header-menu.cjs.map +2 -2
- package/build/components/dataviews-layouts/table/index.cjs +3 -2
- package/build/components/dataviews-layouts/table/index.cjs.map +2 -2
- package/build/components/dataviews-picker-footer/index.cjs +8 -15
- package/build/components/dataviews-picker-footer/index.cjs.map +3 -3
- package/build/field-types/index.cjs +2 -0
- package/build/field-types/index.cjs.map +3 -3
- package/build/field-types/utils/get-filter.cjs +36 -0
- package/build/field-types/utils/get-filter.cjs.map +7 -0
- package/build/hooks/use-report-validity.cjs +39 -0
- package/build/hooks/use-report-validity.cjs.map +7 -0
- package/build/types/field-api.cjs.map +1 -1
- package/build/utils/filter-sort-and-paginate.cjs +6 -174
- package/build/utils/filter-sort-and-paginate.cjs.map +2 -2
- package/build/utils/get-footer-message.cjs +49 -0
- package/build/utils/get-footer-message.cjs.map +7 -0
- package/build/utils/operators.cjs +203 -24
- package/build/utils/operators.cjs.map +2 -2
- package/build-module/components/dataform-controls/combobox.mjs +49 -0
- package/build-module/components/dataform-controls/combobox.mjs.map +7 -0
- package/build-module/components/dataform-controls/date.mjs +35 -10
- package/build-module/components/dataform-controls/date.mjs.map +2 -2
- package/build-module/components/dataform-controls/index.mjs +2 -0
- package/build-module/components/dataform-controls/index.mjs.map +2 -2
- package/build-module/components/dataform-layouts/card/index.mjs +59 -3
- package/build-module/components/dataform-layouts/card/index.mjs.map +2 -2
- package/build-module/components/dataform-layouts/panel/dropdown.mjs +20 -10
- package/build-module/components/dataform-layouts/panel/dropdown.mjs.map +2 -2
- package/build-module/components/dataform-layouts/panel/index.mjs +5 -3
- package/build-module/components/dataform-layouts/panel/index.mjs.map +2 -2
- package/build-module/components/dataform-layouts/panel/modal.mjs +18 -12
- package/build-module/components/dataform-layouts/panel/modal.mjs.map +2 -2
- package/build-module/components/dataviews-bulk-actions/index.mjs +17 -19
- package/build-module/components/dataviews-bulk-actions/index.mjs.map +2 -2
- package/build-module/components/dataviews-item-actions/index.mjs +4 -1
- package/build-module/components/dataviews-item-actions/index.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/activity/activity-item.mjs +6 -1
- package/build-module/components/dataviews-layouts/activity/activity-item.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/table/column-header-menu.mjs +74 -67
- package/build-module/components/dataviews-layouts/table/column-header-menu.mjs.map +2 -2
- package/build-module/components/dataviews-layouts/table/index.mjs +4 -3
- package/build-module/components/dataviews-layouts/table/index.mjs.map +2 -2
- package/build-module/components/dataviews-picker-footer/index.mjs +8 -15
- package/build-module/components/dataviews-picker-footer/index.mjs.map +2 -2
- package/build-module/field-types/index.mjs +2 -0
- package/build-module/field-types/index.mjs.map +2 -2
- package/build-module/field-types/utils/get-filter.mjs +15 -0
- package/build-module/field-types/utils/get-filter.mjs.map +7 -0
- package/build-module/hooks/use-report-validity.mjs +18 -0
- package/build-module/hooks/use-report-validity.mjs.map +7 -0
- package/build-module/utils/filter-sort-and-paginate.mjs +7 -198
- package/build-module/utils/filter-sort-and-paginate.mjs.map +2 -2
- package/build-module/utils/get-footer-message.mjs +28 -0
- package/build-module/utils/get-footer-message.mjs.map +7 -0
- package/build-module/utils/operators.mjs +203 -24
- package/build-module/utils/operators.mjs.map +2 -2
- package/build-style/style-rtl.css +15 -16
- package/build-style/style.css +15 -16
- package/build-types/components/dataform-controls/combobox.d.ts +6 -0
- package/build-types/components/dataform-controls/combobox.d.ts.map +1 -0
- package/build-types/components/dataform-controls/date.d.ts.map +1 -1
- package/build-types/components/dataform-controls/index.d.ts.map +1 -1
- package/build-types/components/dataform-layouts/card/index.d.ts +2 -0
- package/build-types/components/dataform-layouts/card/index.d.ts.map +1 -1
- package/build-types/components/dataform-layouts/panel/dropdown.d.ts +3 -2
- package/build-types/components/dataform-layouts/panel/dropdown.d.ts.map +1 -1
- package/build-types/components/dataform-layouts/panel/index.d.ts.map +1 -1
- package/build-types/components/dataform-layouts/panel/modal.d.ts +3 -2
- package/build-types/components/dataform-layouts/panel/modal.d.ts.map +1 -1
- package/build-types/components/dataviews-bulk-actions/index.d.ts.map +1 -1
- package/build-types/components/dataviews-item-actions/index.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/activity/activity-item.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/table/column-header-menu.d.ts.map +1 -1
- package/build-types/components/dataviews-layouts/table/index.d.ts.map +1 -1
- package/build-types/components/dataviews-picker-footer/index.d.ts.map +1 -1
- package/build-types/dataform/stories/content.story.d.ts +14 -0
- package/build-types/dataform/stories/content.story.d.ts.map +1 -0
- package/build-types/dataform/stories/index.story.d.ts +1 -1
- package/build-types/dataform/stories/index.story.d.ts.map +1 -1
- package/build-types/dataform/stories/validation.d.ts +1 -1
- package/build-types/dataform/stories/validation.d.ts.map +1 -1
- package/build-types/dataviews/stories/fixtures.d.ts.map +1 -1
- package/build-types/dataviews/stories/index.story.d.ts +4 -1
- package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
- package/build-types/dataviews/stories/layout-custom.d.ts +11 -0
- package/build-types/dataviews/stories/layout-custom.d.ts.map +1 -0
- package/build-types/dataviews-picker/stories/fixtures.d.ts.map +1 -1
- package/build-types/dataviews-picker/stories/index.story.d.ts +1 -1
- package/build-types/dataviews-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/field-types/index.d.ts.map +1 -1
- package/build-types/field-types/stories/index.story.d.ts +1 -1
- package/build-types/field-types/stories/index.story.d.ts.map +1 -1
- package/build-types/field-types/utils/get-filter.d.ts +7 -0
- package/build-types/field-types/utils/get-filter.d.ts.map +1 -0
- package/build-types/hooks/use-report-validity.d.ts +14 -0
- package/build-types/hooks/use-report-validity.d.ts.map +1 -0
- package/build-types/types/field-api.d.ts +3 -0
- package/build-types/types/field-api.d.ts.map +1 -1
- package/build-types/utils/filter-sort-and-paginate.d.ts.map +1 -1
- package/build-types/utils/get-footer-message.d.ts +10 -0
- package/build-types/utils/get-footer-message.d.ts.map +1 -0
- package/build-types/utils/operators.d.ts +2 -1
- package/build-types/utils/operators.d.ts.map +1 -1
- package/build-wp/index.js +2730 -2179
- package/package.json +22 -20
- package/src/components/dataform-controls/combobox.tsx +58 -0
- package/src/components/dataform-controls/date.tsx +45 -10
- package/src/components/dataform-controls/index.tsx +2 -0
- package/src/components/dataform-layouts/card/index.tsx +81 -3
- package/src/components/dataform-layouts/panel/dropdown.tsx +26 -11
- package/src/components/dataform-layouts/panel/index.tsx +6 -4
- package/src/components/dataform-layouts/panel/modal.tsx +24 -12
- package/src/components/dataviews-bulk-actions/index.tsx +23 -20
- package/src/components/dataviews-bulk-actions/style.scss +0 -3
- package/src/components/dataviews-item-actions/index.tsx +6 -1
- package/src/components/dataviews-layouts/activity/activity-item.tsx +8 -1
- package/src/components/dataviews-layouts/table/column-header-menu.tsx +99 -73
- package/src/components/dataviews-layouts/table/index.tsx +12 -3
- package/src/components/dataviews-layouts/table/style.scss +14 -7
- package/src/components/dataviews-picker-footer/index.tsx +8 -18
- package/src/dataform/stories/content.story.mdx +159 -0
- package/src/dataform/stories/content.story.tsx +390 -0
- package/src/dataform/stories/index.story.tsx +8 -1
- package/src/dataform/stories/validation.tsx +98 -5
- package/src/dataviews/stories/best-practices.story.mdx +55 -0
- package/src/dataviews/stories/fixtures.tsx +1 -3
- package/src/dataviews/stories/index.story.tsx +6 -1
- package/src/dataviews/stories/layout-custom.tsx +140 -0
- package/src/dataviews/test/dataviews.tsx +66 -1
- package/src/dataviews-picker/stories/fixtures.tsx +1 -3
- package/src/dataviews-picker/stories/index.story.tsx +1 -1
- package/src/field-types/index.tsx +2 -0
- package/src/field-types/stories/index.story.tsx +2 -0
- package/src/field-types/utils/get-filter.ts +18 -0
- package/src/hooks/use-report-validity.ts +32 -0
- package/src/types/field-api.ts +11 -0
- package/src/utils/filter-sort-and-paginate.ts +11 -306
- package/src/utils/get-footer-message.ts +41 -0
- package/src/utils/operators.tsx +303 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordpress/dataviews",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).",
|
|
5
5
|
"author": "The WordPress Contributors",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -53,23 +53,23 @@
|
|
|
53
53
|
"sideEffects": false,
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@ariakit/react": "^0.4.15",
|
|
56
|
-
"@wordpress/base-styles": "^6.
|
|
57
|
-
"@wordpress/components": "^32.
|
|
58
|
-
"@wordpress/compose": "^7.
|
|
59
|
-
"@wordpress/data": "^10.
|
|
60
|
-
"@wordpress/date": "^5.
|
|
61
|
-
"@wordpress/deprecated": "^4.
|
|
62
|
-
"@wordpress/dom": "^4.
|
|
63
|
-
"@wordpress/element": "^6.
|
|
64
|
-
"@wordpress/i18n": "^6.
|
|
65
|
-
"@wordpress/icons": "^11.
|
|
66
|
-
"@wordpress/keycodes": "^4.
|
|
67
|
-
"@wordpress/primitives": "^4.
|
|
68
|
-
"@wordpress/private-apis": "^1.
|
|
69
|
-
"@wordpress/theme": "^0.
|
|
70
|
-
"@wordpress/ui": "^0.
|
|
71
|
-
"@wordpress/url": "^4.
|
|
72
|
-
"@wordpress/warning": "^3.
|
|
56
|
+
"@wordpress/base-styles": "^6.15.0",
|
|
57
|
+
"@wordpress/components": "^32.1.0",
|
|
58
|
+
"@wordpress/compose": "^7.39.0",
|
|
59
|
+
"@wordpress/data": "^10.39.0",
|
|
60
|
+
"@wordpress/date": "^5.39.0",
|
|
61
|
+
"@wordpress/deprecated": "^4.39.0",
|
|
62
|
+
"@wordpress/dom": "^4.39.0",
|
|
63
|
+
"@wordpress/element": "^6.39.0",
|
|
64
|
+
"@wordpress/i18n": "^6.12.0",
|
|
65
|
+
"@wordpress/icons": "^11.6.0",
|
|
66
|
+
"@wordpress/keycodes": "^4.39.0",
|
|
67
|
+
"@wordpress/primitives": "^4.39.0",
|
|
68
|
+
"@wordpress/private-apis": "^1.39.0",
|
|
69
|
+
"@wordpress/theme": "^0.6.0",
|
|
70
|
+
"@wordpress/ui": "^0.6.0",
|
|
71
|
+
"@wordpress/url": "^4.39.0",
|
|
72
|
+
"@wordpress/warning": "^3.39.0",
|
|
73
73
|
"clsx": "^2.1.1",
|
|
74
74
|
"colord": "^2.7.0",
|
|
75
75
|
"date-fns": "^4.1.0",
|
|
@@ -78,10 +78,12 @@
|
|
|
78
78
|
"remove-accents": "^0.5.0"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
+
"@storybook/addon-docs": "^10.1.11",
|
|
82
|
+
"@storybook/react-vite": "^10.1.11",
|
|
81
83
|
"@testing-library/jest-dom": "^6.6.3",
|
|
82
84
|
"@types/jest": "^29.5.14",
|
|
83
85
|
"esbuild": "^0.27.2",
|
|
84
|
-
"storybook": "^
|
|
86
|
+
"storybook": "^10.1.11"
|
|
85
87
|
},
|
|
86
88
|
"peerDependencies": {
|
|
87
89
|
"react": "^18.0.0",
|
|
@@ -93,5 +95,5 @@
|
|
|
93
95
|
"scripts": {
|
|
94
96
|
"build:wp": "node build.cjs"
|
|
95
97
|
},
|
|
96
|
-
"gitHead": "
|
|
98
|
+
"gitHead": "eee1cfb1472f11183e40fb77465a5f13145df7ad"
|
|
97
99
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { privateApis, Spinner } from '@wordpress/components';
|
|
5
|
+
import { useCallback } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import type { DataFormControlProps } from '../../types';
|
|
11
|
+
import useElements from '../../hooks/use-elements';
|
|
12
|
+
import { unlock } from '../../lock-unlock';
|
|
13
|
+
import getCustomValidity from './utils/get-custom-validity';
|
|
14
|
+
|
|
15
|
+
const { ValidatedComboboxControl } = unlock( privateApis );
|
|
16
|
+
|
|
17
|
+
export default function Combobox< Item >( {
|
|
18
|
+
data,
|
|
19
|
+
field,
|
|
20
|
+
onChange,
|
|
21
|
+
hideLabelFromVision,
|
|
22
|
+
validity,
|
|
23
|
+
}: DataFormControlProps< Item > ) {
|
|
24
|
+
const { label, description, placeholder, getValue, setValue, isValid } =
|
|
25
|
+
field;
|
|
26
|
+
const value = getValue( { item: data } ) ?? '';
|
|
27
|
+
|
|
28
|
+
const onChangeControl = useCallback(
|
|
29
|
+
( newValue: string | null ) =>
|
|
30
|
+
onChange( setValue( { item: data, value: newValue ?? '' } ) ),
|
|
31
|
+
[ data, onChange, setValue ]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const { elements, isLoading } = useElements( {
|
|
35
|
+
elements: field.elements,
|
|
36
|
+
getElements: field.getElements,
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
if ( isLoading ) {
|
|
40
|
+
return <Spinner />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ValidatedComboboxControl
|
|
45
|
+
required={ !! field.isValid?.required }
|
|
46
|
+
customValidity={ getCustomValidity( isValid, validity ) }
|
|
47
|
+
label={ label }
|
|
48
|
+
value={ value }
|
|
49
|
+
help={ description }
|
|
50
|
+
placeholder={ placeholder }
|
|
51
|
+
options={ elements }
|
|
52
|
+
onChange={ onChangeControl }
|
|
53
|
+
hideLabelFromVision={ hideLabelFromVision }
|
|
54
|
+
allowReset
|
|
55
|
+
expandOnFocus
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -191,18 +191,53 @@ function ValidatedDateControl< Item >( {
|
|
|
191
191
|
setCustomValidity( undefined );
|
|
192
192
|
}, [ inputRefs ] );
|
|
193
193
|
|
|
194
|
+
// Sync React-level validation to native inputs.
|
|
194
195
|
useEffect( () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
const refs = Array.isArray( inputRefs ) ? inputRefs : [ inputRefs ];
|
|
197
|
+
const result = validity
|
|
198
|
+
? getCustomValidity( isValid, validity )
|
|
199
|
+
: undefined;
|
|
200
|
+
for ( const ref of refs ) {
|
|
201
|
+
const input = ref.current;
|
|
202
|
+
if ( input ) {
|
|
203
|
+
input.setCustomValidity(
|
|
204
|
+
result?.type === 'invalid' && result.message
|
|
205
|
+
? result.message
|
|
206
|
+
: ''
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}, [ inputRefs, isValid, validity ] );
|
|
211
|
+
|
|
212
|
+
// Listen for 'invalid' events (e.g., from reportValidity() on card re-expand).
|
|
213
|
+
useEffect( () => {
|
|
214
|
+
const refs = Array.isArray( inputRefs ) ? inputRefs : [ inputRefs ];
|
|
215
|
+
const handleInvalid = ( event: Event ) => {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setIsTouched( true );
|
|
218
|
+
};
|
|
219
|
+
for ( const ref of refs ) {
|
|
220
|
+
ref.current?.addEventListener( 'invalid', handleInvalid );
|
|
221
|
+
}
|
|
222
|
+
return () => {
|
|
223
|
+
for ( const ref of refs ) {
|
|
224
|
+
ref.current?.removeEventListener( 'invalid', handleInvalid );
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}, [ inputRefs, setIsTouched ] );
|
|
228
|
+
|
|
229
|
+
useEffect( () => {
|
|
230
|
+
if ( ! isTouched ) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const result = validity
|
|
234
|
+
? getCustomValidity( isValid, validity )
|
|
235
|
+
: undefined;
|
|
236
|
+
if ( result ) {
|
|
237
|
+
setCustomValidity( result );
|
|
238
|
+
} else {
|
|
239
|
+
validateRefs();
|
|
204
240
|
}
|
|
205
|
-
return undefined;
|
|
206
241
|
}, [ isTouched, isValid, validity, validateRefs ] );
|
|
207
242
|
|
|
208
243
|
const onBlur = ( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
@@ -8,6 +8,7 @@ import type { ComponentType } from 'react';
|
|
|
8
8
|
*/
|
|
9
9
|
import type { DataFormControlProps, Field, EditConfig } from '../../types';
|
|
10
10
|
import checkbox from './checkbox';
|
|
11
|
+
import combobox from './combobox';
|
|
11
12
|
import datetime from './datetime';
|
|
12
13
|
import date from './date';
|
|
13
14
|
import email from './email';
|
|
@@ -34,6 +35,7 @@ const FORM_CONTROLS: FormControls = {
|
|
|
34
35
|
array,
|
|
35
36
|
checkbox,
|
|
36
37
|
color,
|
|
38
|
+
combobox,
|
|
37
39
|
datetime,
|
|
38
40
|
date,
|
|
39
41
|
email,
|
|
@@ -7,14 +7,17 @@ import {
|
|
|
7
7
|
CardBody,
|
|
8
8
|
CardHeader as OriginalCardHeader,
|
|
9
9
|
} from '@wordpress/components';
|
|
10
|
+
import { Badge } from '@wordpress/ui';
|
|
10
11
|
import {
|
|
11
12
|
useCallback,
|
|
12
13
|
useContext,
|
|
13
14
|
useEffect,
|
|
14
15
|
useMemo,
|
|
16
|
+
useRef,
|
|
15
17
|
useState,
|
|
16
18
|
} from '@wordpress/element';
|
|
17
19
|
import { chevronDown, chevronUp } from '@wordpress/icons';
|
|
20
|
+
import { sprintf, _n } from '@wordpress/i18n';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Internal dependencies
|
|
@@ -23,6 +26,7 @@ import { getFormFieldLayout } from '..';
|
|
|
23
26
|
import DataFormContext from '../../dataform-context';
|
|
24
27
|
import type {
|
|
25
28
|
FieldLayoutProps,
|
|
29
|
+
FieldValidity,
|
|
26
30
|
NormalizedCardLayout,
|
|
27
31
|
NormalizedField,
|
|
28
32
|
NormalizedForm,
|
|
@@ -31,6 +35,34 @@ import type {
|
|
|
31
35
|
import { DataFormLayout } from '../data-form-layout';
|
|
32
36
|
import { DEFAULT_LAYOUT } from '../normalize-form';
|
|
33
37
|
import { getSummaryFields } from '../get-summary-fields';
|
|
38
|
+
import useReportValidity from '../../../hooks/use-report-validity';
|
|
39
|
+
|
|
40
|
+
function countInvalidFields( validity: FieldValidity | undefined ): number {
|
|
41
|
+
if ( ! validity ) {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let count = 0;
|
|
46
|
+
const validityRules = Object.keys( validity ).filter(
|
|
47
|
+
( key ) => key !== 'children'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
for ( const key of validityRules ) {
|
|
51
|
+
const rule = validity[ key as keyof Omit< FieldValidity, 'children' > ];
|
|
52
|
+
if ( rule?.type === 'invalid' ) {
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Count children recursively
|
|
58
|
+
if ( validity.children ) {
|
|
59
|
+
for ( const childValidity of Object.values( validity.children ) ) {
|
|
60
|
+
count += countInvalidFields( childValidity );
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
34
66
|
|
|
35
67
|
const NonCollapsibleCardHeader = ( {
|
|
36
68
|
children,
|
|
@@ -56,6 +88,7 @@ const NonCollapsibleCardHeader = ( {
|
|
|
56
88
|
export function useCardHeader( layout: NormalizedCardLayout ) {
|
|
57
89
|
const { isOpened, isCollapsible } = layout;
|
|
58
90
|
const [ isOpen, setIsOpen ] = useState( isOpened );
|
|
91
|
+
const [ touched, setTouched ] = useState( false );
|
|
59
92
|
|
|
60
93
|
// Sync internal state when the isOpened prop changes.
|
|
61
94
|
// This is unlikely to happen in production, but it helps with storybook controls.
|
|
@@ -64,8 +97,12 @@ export function useCardHeader( layout: NormalizedCardLayout ) {
|
|
|
64
97
|
}, [ isOpened ] );
|
|
65
98
|
|
|
66
99
|
const toggle = useCallback( () => {
|
|
100
|
+
// Mark as touched when collapsing (going from open to closed)
|
|
101
|
+
if ( isOpen ) {
|
|
102
|
+
setTouched( true );
|
|
103
|
+
}
|
|
67
104
|
setIsOpen( ( prev ) => ! prev );
|
|
68
|
-
}, [] );
|
|
105
|
+
}, [ isOpen ] );
|
|
69
106
|
|
|
70
107
|
const CollapsibleCardHeader = useCallback(
|
|
71
108
|
( {
|
|
@@ -111,7 +148,12 @@ export function useCardHeader( layout: NormalizedCardLayout ) {
|
|
|
111
148
|
? CollapsibleCardHeader
|
|
112
149
|
: NonCollapsibleCardHeader;
|
|
113
150
|
|
|
114
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
isOpen: effectiveIsOpen,
|
|
153
|
+
CardHeader: CardHeaderComponent,
|
|
154
|
+
touched,
|
|
155
|
+
setTouched,
|
|
156
|
+
};
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
function isSummaryFieldVisible< Item >(
|
|
@@ -174,6 +216,7 @@ export default function FormCardField< Item >( {
|
|
|
174
216
|
}: FieldLayoutProps< Item > ) {
|
|
175
217
|
const { fields } = useContext( DataFormContext );
|
|
176
218
|
const layout = field.layout as NormalizedCardLayout;
|
|
219
|
+
const cardBodyRef = useRef< HTMLDivElement >( null );
|
|
177
220
|
|
|
178
221
|
const form: NormalizedForm = useMemo(
|
|
179
222
|
() => ( {
|
|
@@ -183,7 +226,17 @@ export default function FormCardField< Item >( {
|
|
|
183
226
|
[ field ]
|
|
184
227
|
);
|
|
185
228
|
|
|
186
|
-
const { isOpen, CardHeader } = useCardHeader( layout );
|
|
229
|
+
const { isOpen, CardHeader, touched, setTouched } = useCardHeader( layout );
|
|
230
|
+
|
|
231
|
+
// Mark the card as touched when any field inside it is blurred.
|
|
232
|
+
// This aligns with how validated controls show errors on blur.
|
|
233
|
+
const handleBlur = useCallback( () => {
|
|
234
|
+
setTouched( true );
|
|
235
|
+
}, [ setTouched ] );
|
|
236
|
+
|
|
237
|
+
// When the card is expanded after being touched (collapsed with errors),
|
|
238
|
+
// trigger reportValidity to show field-level errors.
|
|
239
|
+
useReportValidity( cardBodyRef, isOpen && touched );
|
|
187
240
|
|
|
188
241
|
const summaryFields = getSummaryFields< Item >( layout.summary, fields );
|
|
189
242
|
|
|
@@ -191,6 +244,25 @@ export default function FormCardField< Item >( {
|
|
|
191
244
|
isSummaryFieldVisible( summaryField, layout.summary, isOpen )
|
|
192
245
|
);
|
|
193
246
|
|
|
247
|
+
// Count invalid fields for validation badge
|
|
248
|
+
const invalidCount = countInvalidFields( validity );
|
|
249
|
+
const showValidationBadge =
|
|
250
|
+
touched && invalidCount > 0 && layout.isCollapsible;
|
|
251
|
+
|
|
252
|
+
const validationBadge = showValidationBadge ? (
|
|
253
|
+
<Badge intent="high">
|
|
254
|
+
{ sprintf(
|
|
255
|
+
/* translators: %d: Number of fields that need attention */
|
|
256
|
+
_n(
|
|
257
|
+
'%d field needs attention',
|
|
258
|
+
'%d fields need attention',
|
|
259
|
+
invalidCount
|
|
260
|
+
),
|
|
261
|
+
invalidCount
|
|
262
|
+
) }
|
|
263
|
+
</Badge>
|
|
264
|
+
) : null;
|
|
265
|
+
|
|
194
266
|
const sizeCard = {
|
|
195
267
|
blockStart: 'medium' as const,
|
|
196
268
|
blockEnd: 'medium' as const,
|
|
@@ -217,6 +289,7 @@ export default function FormCardField< Item >( {
|
|
|
217
289
|
<span className="dataforms-layouts-card__field-header-label">
|
|
218
290
|
{ field.label }
|
|
219
291
|
</span>
|
|
292
|
+
{ validationBadge }
|
|
220
293
|
{ visibleSummaryFields.length > 0 &&
|
|
221
294
|
layout.withHeader && (
|
|
222
295
|
<div className="dataforms-layouts-card__field-summary">
|
|
@@ -239,6 +312,8 @@ export default function FormCardField< Item >( {
|
|
|
239
312
|
<CardBody
|
|
240
313
|
size={ sizeCardBody }
|
|
241
314
|
className="dataforms-layouts-card__field-control"
|
|
315
|
+
ref={ cardBodyRef }
|
|
316
|
+
onBlur={ handleBlur }
|
|
242
317
|
>
|
|
243
318
|
{ field.description && (
|
|
244
319
|
<div className="dataforms-layouts-card__field-description">
|
|
@@ -285,6 +360,7 @@ export default function FormCardField< Item >( {
|
|
|
285
360
|
<span className="dataforms-layouts-card__field-header-label">
|
|
286
361
|
{ fieldDefinition.label }
|
|
287
362
|
</span>
|
|
363
|
+
{ validationBadge }
|
|
288
364
|
{ visibleSummaryFields.length > 0 && layout.withHeader && (
|
|
289
365
|
<div className="dataforms-layouts-card__field-summary">
|
|
290
366
|
{ visibleSummaryFields.map( ( summaryField ) => (
|
|
@@ -304,6 +380,8 @@ export default function FormCardField< Item >( {
|
|
|
304
380
|
<CardBody
|
|
305
381
|
size={ sizeCardBody }
|
|
306
382
|
className="dataforms-layouts-card__field-control"
|
|
383
|
+
ref={ cardBodyRef }
|
|
384
|
+
onBlur={ handleBlur }
|
|
307
385
|
>
|
|
308
386
|
<RegularLayout
|
|
309
387
|
data={ data }
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Button,
|
|
9
9
|
} from '@wordpress/components';
|
|
10
10
|
import { __ } from '@wordpress/i18n';
|
|
11
|
-
import { useMemo } from '@wordpress/element';
|
|
11
|
+
import { useMemo, useRef } from '@wordpress/element';
|
|
12
12
|
import { closeSmall } from '@wordpress/icons';
|
|
13
13
|
import { useFocusOnMount } from '@wordpress/compose';
|
|
14
14
|
import { Stack } from '@wordpress/ui';
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
import { DataFormLayout } from '../data-form-layout';
|
|
27
27
|
import { DEFAULT_LAYOUT } from '../normalize-form';
|
|
28
28
|
import SummaryButton from './summary-button';
|
|
29
|
+
import useReportValidity from '../../../hooks/use-report-validity';
|
|
29
30
|
|
|
30
31
|
function DropdownHeader( {
|
|
31
32
|
title,
|
|
@@ -60,6 +61,18 @@ function DropdownHeader( {
|
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function DropdownContentWithValidation( {
|
|
65
|
+
touched,
|
|
66
|
+
children,
|
|
67
|
+
}: {
|
|
68
|
+
touched: boolean;
|
|
69
|
+
children: React.ReactNode;
|
|
70
|
+
} ) {
|
|
71
|
+
const ref = useRef< HTMLDivElement >( null );
|
|
72
|
+
useReportValidity( ref, touched );
|
|
73
|
+
return <div ref={ ref }>{ children }</div>;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
function PanelDropdown< Item >( {
|
|
64
77
|
data,
|
|
65
78
|
field,
|
|
@@ -69,7 +82,8 @@ function PanelDropdown< Item >( {
|
|
|
69
82
|
summaryFields,
|
|
70
83
|
fieldDefinition,
|
|
71
84
|
popoverAnchor,
|
|
72
|
-
|
|
85
|
+
onClose: onCloseCallback,
|
|
86
|
+
touched,
|
|
73
87
|
}: {
|
|
74
88
|
data: Item;
|
|
75
89
|
field: NormalizedFormField;
|
|
@@ -79,7 +93,8 @@ function PanelDropdown< Item >( {
|
|
|
79
93
|
summaryFields: NormalizedField< Item >[];
|
|
80
94
|
fieldDefinition: NormalizedField< Item >;
|
|
81
95
|
popoverAnchor: HTMLElement | null;
|
|
82
|
-
|
|
96
|
+
onClose?: () => void;
|
|
97
|
+
touched: boolean;
|
|
83
98
|
} ) {
|
|
84
99
|
const fieldLabel = !! field.children ? field.label : fieldDefinition?.label;
|
|
85
100
|
|
|
@@ -125,6 +140,11 @@ function PanelDropdown< Item >( {
|
|
|
125
140
|
contentClassName="dataforms-layouts-panel__field-dropdown"
|
|
126
141
|
popoverProps={ popoverProps }
|
|
127
142
|
focusOnMount={ false }
|
|
143
|
+
onToggle={ ( willOpen ) => {
|
|
144
|
+
if ( ! willOpen ) {
|
|
145
|
+
onCloseCallback?.();
|
|
146
|
+
}
|
|
147
|
+
} }
|
|
128
148
|
toggleProps={ {
|
|
129
149
|
size: 'compact',
|
|
130
150
|
variant: 'tertiary',
|
|
@@ -137,17 +157,12 @@ function PanelDropdown< Item >( {
|
|
|
137
157
|
labelPosition={ labelPosition }
|
|
138
158
|
fieldLabel={ fieldLabel }
|
|
139
159
|
disabled={ fieldDefinition.readOnly === true }
|
|
140
|
-
onClick={
|
|
141
|
-
if ( ! isOpen && onOpen ) {
|
|
142
|
-
onOpen();
|
|
143
|
-
}
|
|
144
|
-
onToggle();
|
|
145
|
-
} }
|
|
160
|
+
onClick={ onToggle }
|
|
146
161
|
aria-expanded={ isOpen }
|
|
147
162
|
/>
|
|
148
163
|
) }
|
|
149
164
|
renderContent={ ( { onClose } ) => (
|
|
150
|
-
|
|
165
|
+
<DropdownContentWithValidation touched={ touched }>
|
|
151
166
|
<DropdownHeader title={ fieldLabel } onClose={ onClose } />
|
|
152
167
|
<div ref={ focusOnMountRef }>
|
|
153
168
|
<DataFormLayout
|
|
@@ -174,7 +189,7 @@ function PanelDropdown< Item >( {
|
|
|
174
189
|
) }
|
|
175
190
|
</DataFormLayout>
|
|
176
191
|
</div>
|
|
177
|
-
|
|
192
|
+
</DropdownContentWithValidation>
|
|
178
193
|
) }
|
|
179
194
|
/>
|
|
180
195
|
);
|
|
@@ -147,9 +147,9 @@ export default function FormPanelField< Item >( {
|
|
|
147
147
|
null
|
|
148
148
|
);
|
|
149
149
|
|
|
150
|
-
// Track if the panel has been
|
|
150
|
+
// Track if the panel has been closed (touched) to only show errors after interaction.
|
|
151
151
|
const [ touched, setTouched ] = useState( false );
|
|
152
|
-
const
|
|
152
|
+
const handleClose = () => setTouched( true );
|
|
153
153
|
|
|
154
154
|
const { fieldDefinition, summaryFields } =
|
|
155
155
|
getFieldDefinitionAndSummaryFields( layout, field, fields );
|
|
@@ -193,7 +193,8 @@ export default function FormPanelField< Item >( {
|
|
|
193
193
|
labelPosition={ labelPosition }
|
|
194
194
|
summaryFields={ summaryFields }
|
|
195
195
|
fieldDefinition={ fieldDefinition }
|
|
196
|
-
|
|
196
|
+
onClose={ handleClose }
|
|
197
|
+
touched={ touched }
|
|
197
198
|
/>
|
|
198
199
|
) : (
|
|
199
200
|
<PanelDropdown
|
|
@@ -205,7 +206,8 @@ export default function FormPanelField< Item >( {
|
|
|
205
206
|
summaryFields={ summaryFields }
|
|
206
207
|
fieldDefinition={ fieldDefinition }
|
|
207
208
|
popoverAnchor={ popoverAnchor }
|
|
208
|
-
|
|
209
|
+
onClose={ handleClose }
|
|
210
|
+
touched={ touched }
|
|
209
211
|
/>
|
|
210
212
|
);
|
|
211
213
|
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
Modal,
|
|
13
13
|
} from '@wordpress/components';
|
|
14
14
|
import { __ } from '@wordpress/i18n';
|
|
15
|
-
import { useContext,
|
|
16
|
-
import { useFocusOnMount } from '@wordpress/compose';
|
|
15
|
+
import { useContext, useMemo, useRef, useState } from '@wordpress/element';
|
|
16
|
+
import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
|
|
17
17
|
import { Stack } from '@wordpress/ui';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -29,6 +29,7 @@ import { DataFormLayout } from '../data-form-layout';
|
|
|
29
29
|
import { DEFAULT_LAYOUT } from '../normalize-form';
|
|
30
30
|
import SummaryButton from './summary-button';
|
|
31
31
|
import useFormValidity from '../../../hooks/use-form-validity';
|
|
32
|
+
import useReportValidity from '../../../hooks/use-report-validity';
|
|
32
33
|
import DataFormContext from '../../dataform-context';
|
|
33
34
|
|
|
34
35
|
function ModalContent< Item >( {
|
|
@@ -37,12 +38,14 @@ function ModalContent< Item >( {
|
|
|
37
38
|
onChange,
|
|
38
39
|
fieldLabel,
|
|
39
40
|
onClose,
|
|
41
|
+
touched,
|
|
40
42
|
}: {
|
|
41
43
|
data: Item;
|
|
42
44
|
field: NormalizedFormField;
|
|
43
45
|
onChange: ( data: Partial< Item > ) => void;
|
|
44
46
|
onClose: () => void;
|
|
45
47
|
fieldLabel: string;
|
|
48
|
+
touched: boolean;
|
|
46
49
|
} ) {
|
|
47
50
|
const { fields } = useContext( DataFormContext );
|
|
48
51
|
const [ changes, setChanges ] = useState< Partial< Item > >( {} );
|
|
@@ -92,6 +95,12 @@ function ModalContent< Item >( {
|
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
const focusOnMountRef = useFocusOnMount( 'firstInputElement' );
|
|
98
|
+
const contentRef = useRef< HTMLDivElement >( null );
|
|
99
|
+
const mergedRef = useMergeRefs( [ focusOnMountRef, contentRef ] );
|
|
100
|
+
|
|
101
|
+
// When the modal is opened after being previously closed (touched),
|
|
102
|
+
// trigger reportValidity to show field-level errors.
|
|
103
|
+
useReportValidity( contentRef, touched );
|
|
95
104
|
|
|
96
105
|
return (
|
|
97
106
|
<Modal
|
|
@@ -101,7 +110,7 @@ function ModalContent< Item >( {
|
|
|
101
110
|
title={ fieldLabel }
|
|
102
111
|
size="medium"
|
|
103
112
|
>
|
|
104
|
-
<div ref={
|
|
113
|
+
<div ref={ mergedRef }>
|
|
105
114
|
<DataFormLayout
|
|
106
115
|
data={ modalData }
|
|
107
116
|
form={ form }
|
|
@@ -152,7 +161,8 @@ function PanelModal< Item >( {
|
|
|
152
161
|
labelPosition,
|
|
153
162
|
summaryFields,
|
|
154
163
|
fieldDefinition,
|
|
155
|
-
|
|
164
|
+
onClose: onCloseCallback,
|
|
165
|
+
touched,
|
|
156
166
|
}: {
|
|
157
167
|
data: Item;
|
|
158
168
|
field: NormalizedFormField;
|
|
@@ -160,12 +170,18 @@ function PanelModal< Item >( {
|
|
|
160
170
|
labelPosition: 'side' | 'top' | 'none';
|
|
161
171
|
summaryFields: NormalizedField< Item >[];
|
|
162
172
|
fieldDefinition: NormalizedField< Item >;
|
|
163
|
-
|
|
173
|
+
onClose?: () => void;
|
|
174
|
+
touched: boolean;
|
|
164
175
|
} ) {
|
|
165
176
|
const [ isOpen, setIsOpen ] = useState( false );
|
|
166
177
|
|
|
167
178
|
const fieldLabel = !! field.children ? field.label : fieldDefinition?.label;
|
|
168
179
|
|
|
180
|
+
const handleClose = () => {
|
|
181
|
+
setIsOpen( false );
|
|
182
|
+
onCloseCallback?.();
|
|
183
|
+
};
|
|
184
|
+
|
|
169
185
|
return (
|
|
170
186
|
<>
|
|
171
187
|
<SummaryButton
|
|
@@ -174,12 +190,7 @@ function PanelModal< Item >( {
|
|
|
174
190
|
labelPosition={ labelPosition }
|
|
175
191
|
fieldLabel={ fieldLabel }
|
|
176
192
|
disabled={ fieldDefinition.readOnly === true }
|
|
177
|
-
onClick={ () =>
|
|
178
|
-
if ( onOpen ) {
|
|
179
|
-
onOpen();
|
|
180
|
-
}
|
|
181
|
-
setIsOpen( true );
|
|
182
|
-
} }
|
|
193
|
+
onClick={ () => setIsOpen( true ) }
|
|
183
194
|
aria-expanded={ isOpen }
|
|
184
195
|
/>
|
|
185
196
|
{ isOpen && (
|
|
@@ -188,7 +199,8 @@ function PanelModal< Item >( {
|
|
|
188
199
|
field={ field }
|
|
189
200
|
onChange={ onChange }
|
|
190
201
|
fieldLabel={ fieldLabel ?? '' }
|
|
191
|
-
onClose={
|
|
202
|
+
onClose={ handleClose }
|
|
203
|
+
touched={ touched }
|
|
192
204
|
/>
|
|
193
205
|
) }
|
|
194
206
|
</>
|