@wordpress/components 30.2.1-next.f34ab90e9.0 → 30.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 +18 -1
- package/build/card/context.js +1 -0
- package/build/card/context.js.map +1 -1
- package/build/circular-option-picker/circular-option-picker-context.js +1 -0
- package/build/circular-option-picker/circular-option-picker-context.js.map +1 -1
- package/build/composite/context.js +1 -0
- package/build/composite/context.js.map +1 -1
- package/build/context/context-connect.js.map +1 -1
- package/build/context/context-system-provider.js +1 -0
- package/build/context/context-system-provider.js.map +1 -1
- package/build/custom-select-control-v2/custom-select.js +1 -0
- package/build/custom-select-control-v2/custom-select.js.map +1 -1
- package/build/disabled/index.js +1 -0
- package/build/disabled/index.js.map +1 -1
- package/build/item-group/context.js +1 -0
- package/build/item-group/context.js.map +1 -1
- package/build/menu/context.js +1 -0
- package/build/menu/context.js.map +1 -1
- package/build/modal/index.js +1 -0
- package/build/modal/index.js.map +1 -1
- package/build/navigation/context.js +1 -0
- package/build/navigation/context.js.map +1 -1
- package/build/navigation/group/context.js +1 -0
- package/build/navigation/group/context.js.map +1 -1
- package/build/navigation/menu/context.js +1 -0
- package/build/navigation/menu/context.js.map +1 -1
- package/build/navigator/context.js +1 -0
- package/build/navigator/context.js.map +1 -1
- package/build/popover/index.js +1 -0
- package/build/popover/index.js.map +1 -1
- package/build/radio-group/context.js +1 -0
- package/build/radio-group/context.js.map +1 -1
- package/build/slot-fill/bubbles-virtually/slot-fill-context.js +1 -0
- package/build/slot-fill/bubbles-virtually/slot-fill-context.js.map +1 -1
- package/build/slot-fill/context.js +1 -0
- package/build/slot-fill/context.js.map +1 -1
- package/build/tabs/context.js +1 -0
- package/build/tabs/context.js.map +1 -1
- package/build/tabs/styles.js +5 -5
- package/build/tabs/styles.js.map +1 -1
- package/build/toggle-group-control/context.js +1 -0
- package/build/toggle-group-control/context.js.map +1 -1
- package/build/toolbar/toolbar-context/index.js +1 -0
- package/build/toolbar/toolbar-context/index.js.map +1 -1
- package/build/tools-panel/context.js +1 -0
- package/build/tools-panel/context.js.map +1 -1
- package/build/tooltip/index.js +1 -0
- package/build/tooltip/index.js.map +1 -1
- package/build/tree-grid/roving-tab-index-context.js +1 -0
- package/build/tree-grid/roving-tab-index-context.js.map +1 -1
- package/build/utils/font-size.js.map +1 -1
- package/build/utils/get-valid-children.js.map +1 -1
- package/build/validated-form-controls/control-with-error.js +16 -16
- package/build/validated-form-controls/control-with-error.js.map +1 -1
- package/build-module/card/context.js +1 -0
- package/build-module/card/context.js.map +1 -1
- package/build-module/circular-option-picker/circular-option-picker-context.js +1 -0
- package/build-module/circular-option-picker/circular-option-picker-context.js.map +1 -1
- package/build-module/composite/context.js +1 -0
- package/build-module/composite/context.js.map +1 -1
- package/build-module/context/context-connect.js.map +1 -1
- package/build-module/context/context-system-provider.js +1 -0
- package/build-module/context/context-system-provider.js.map +1 -1
- package/build-module/custom-select-control-v2/custom-select.js +1 -0
- package/build-module/custom-select-control-v2/custom-select.js.map +1 -1
- package/build-module/disabled/index.js +1 -0
- package/build-module/disabled/index.js.map +1 -1
- package/build-module/item-group/context.js +1 -0
- package/build-module/item-group/context.js.map +1 -1
- package/build-module/menu/context.js +1 -0
- package/build-module/menu/context.js.map +1 -1
- package/build-module/modal/index.js +1 -0
- package/build-module/modal/index.js.map +1 -1
- package/build-module/navigation/context.js +1 -0
- package/build-module/navigation/context.js.map +1 -1
- package/build-module/navigation/group/context.js +1 -0
- package/build-module/navigation/group/context.js.map +1 -1
- package/build-module/navigation/menu/context.js +1 -0
- package/build-module/navigation/menu/context.js.map +1 -1
- package/build-module/navigator/context.js +1 -0
- package/build-module/navigator/context.js.map +1 -1
- package/build-module/popover/index.js +1 -0
- package/build-module/popover/index.js.map +1 -1
- package/build-module/radio-group/context.js +1 -0
- package/build-module/radio-group/context.js.map +1 -1
- package/build-module/slot-fill/bubbles-virtually/slot-fill-context.js +1 -0
- package/build-module/slot-fill/bubbles-virtually/slot-fill-context.js.map +1 -1
- package/build-module/slot-fill/context.js +1 -0
- package/build-module/slot-fill/context.js.map +1 -1
- package/build-module/tabs/context.js +1 -0
- package/build-module/tabs/context.js.map +1 -1
- package/build-module/tabs/styles.js +6 -6
- package/build-module/tabs/styles.js.map +1 -1
- package/build-module/toggle-group-control/context.js +1 -0
- package/build-module/toggle-group-control/context.js.map +1 -1
- package/build-module/toolbar/toolbar-context/index.js +1 -0
- package/build-module/toolbar/toolbar-context/index.js.map +1 -1
- package/build-module/tools-panel/context.js +1 -0
- package/build-module/tools-panel/context.js.map +1 -1
- package/build-module/tooltip/index.js +1 -0
- package/build-module/tooltip/index.js.map +1 -1
- package/build-module/tree-grid/roving-tab-index-context.js +1 -0
- package/build-module/tree-grid/roving-tab-index-context.js.map +1 -1
- package/build-module/utils/font-size.js.map +1 -1
- package/build-module/utils/get-valid-children.js.map +1 -1
- package/build-module/validated-form-controls/control-with-error.js +16 -16
- package/build-module/validated-form-controls/control-with-error.js.map +1 -1
- package/build-types/calendar/stories/date-calendar.story.d.ts.map +1 -1
- package/build-types/calendar/stories/date-range-calendar.story.d.ts.map +1 -1
- package/build-types/card/context.d.ts.map +1 -1
- package/build-types/composite/context.d.ts.map +1 -1
- package/build-types/context/context-connect.d.ts +2 -2
- package/build-types/context/context-connect.d.ts.map +1 -1
- package/build-types/context/context-system-provider.d.ts.map +1 -1
- package/build-types/custom-select-control-v2/custom-select.d.ts.map +1 -1
- package/build-types/disabled/index.d.ts.map +1 -1
- package/build-types/item-group/context.d.ts.map +1 -1
- package/build-types/modal/index.d.ts.map +1 -1
- package/build-types/navigation/context.d.ts.map +1 -1
- package/build-types/navigation/group/context.d.ts.map +1 -1
- package/build-types/navigation/menu/context.d.ts.map +1 -1
- package/build-types/popover/index.d.ts +1 -1
- package/build-types/popover/index.d.ts.map +1 -1
- package/build-types/popover/stories/e2e/index.story.d.ts +1 -1
- package/build-types/slot-fill/bubbles-virtually/slot-fill-context.d.ts.map +1 -1
- package/build-types/slot-fill/context.d.ts.map +1 -1
- package/build-types/tabs/context.d.ts.map +1 -1
- package/build-types/tabs/styles.d.ts.map +1 -1
- package/build-types/toggle-group-control/context.d.ts.map +1 -1
- package/build-types/toolbar/toolbar-context/index.d.ts.map +1 -1
- package/build-types/tools-panel/context.d.ts.map +1 -1
- package/build-types/tooltip/index.d.ts.map +1 -1
- package/build-types/tree-grid/roving-tab-index-context.d.ts.map +1 -1
- package/build-types/utils/font-size.d.ts +2 -2
- package/build-types/utils/font-size.d.ts.map +1 -1
- package/build-types/utils/get-valid-children.d.ts +2 -2
- package/build-types/utils/get-valid-children.d.ts.map +1 -1
- package/build-types/validated-form-controls/components/stories/overview.story.d.ts.map +1 -1
- package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
- package/build-types/validated-form-controls/test/control-with-error.d.ts +2 -0
- package/build-types/validated-form-controls/test/control-with-error.d.ts.map +1 -0
- package/package.json +20 -20
- package/src/calendar/stories/date-calendar.story.tsx +1 -0
- package/src/calendar/stories/date-range-calendar.story.tsx +1 -0
- package/src/card/context.ts +2 -0
- package/src/circular-option-picker/circular-option-picker-context.tsx +1 -0
- package/src/composite/context.tsx +1 -0
- package/src/context/context-connect.ts +2 -2
- package/src/context/context-system-provider.js +2 -0
- package/src/custom-select-control-v2/custom-select.tsx +1 -0
- package/src/disabled/index.tsx +2 -0
- package/src/item-group/context.ts +1 -0
- package/src/menu/context.tsx +1 -0
- package/src/modal/index.tsx +1 -0
- package/src/navigation/context.tsx +3 -0
- package/src/navigation/group/context.tsx +1 -0
- package/src/navigation/menu/context.tsx +2 -0
- package/src/navigator/context.ts +1 -0
- package/src/popover/index.tsx +1 -0
- package/src/radio-group/context.tsx +1 -0
- package/src/slot-fill/bubbles-virtually/slot-fill-context.ts +1 -0
- package/src/slot-fill/context.ts +1 -0
- package/src/tabs/context.ts +1 -0
- package/src/tabs/styles.ts +2 -1
- package/src/toggle-group-control/context.ts +2 -0
- package/src/toolbar/toolbar-context/index.ts +1 -0
- package/src/tools-panel/context.ts +1 -0
- package/src/tools-panel/stories/index.story.tsx +3 -3
- package/src/tooltip/index.tsx +1 -0
- package/src/tree-grid/roving-tab-index-context.ts +2 -0
- package/src/utils/font-size.ts +2 -2
- package/src/utils/get-valid-children.ts +4 -2
- package/src/validated-form-controls/components/stories/overview.story.tsx +109 -27
- package/src/validated-form-controls/control-with-error.tsx +19 -18
- package/src/validated-form-controls/test/control-with-error.tsx +224 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,6 +11,8 @@ import type { ToggleGroupControlContextProps } from './types';
|
|
|
11
11
|
const ToggleGroupControlContext = createContext(
|
|
12
12
|
{} as ToggleGroupControlContextProps
|
|
13
13
|
);
|
|
14
|
+
ToggleGroupControlContext.displayName = 'ToggleGroupControlContext';
|
|
15
|
+
|
|
14
16
|
export const useToggleGroupControlContext = () =>
|
|
15
17
|
useContext( ToggleGroupControlContext );
|
|
16
18
|
export default ToggleGroupControlContext;
|
|
@@ -22,6 +22,7 @@ export const ToolsPanelContext = createContext< ToolsPanelContextType >( {
|
|
|
22
22
|
deregisterResetAllFilter: noop,
|
|
23
23
|
areAllOptionalControlsHidden: true,
|
|
24
24
|
} );
|
|
25
|
+
ToolsPanelContext.displayName = 'ToolsPanelContext';
|
|
25
26
|
|
|
26
27
|
export const useToolsPanelContext = () =>
|
|
27
28
|
useContext< ToolsPanelContextType >( ToolsPanelContext );
|
|
@@ -54,7 +54,7 @@ export const Default: StoryFn< typeof ToolsPanel > = ( {
|
|
|
54
54
|
const [ height, setHeight ] = useState< string | undefined >();
|
|
55
55
|
const [ minHeight, setMinHeight ] = useState< string | undefined >();
|
|
56
56
|
const [ width, setWidth ] = useState< string | undefined >();
|
|
57
|
-
const [ scale, setScale ] = useState<
|
|
57
|
+
const [ scale, setScale ] = useState< number | string | undefined >();
|
|
58
58
|
|
|
59
59
|
const resetAll: typeof resetAllProp = ( filters ) => {
|
|
60
60
|
setHeight( undefined );
|
|
@@ -414,7 +414,7 @@ export const WithConditionalDefaultControl: StoryFn< typeof ToolsPanel > = ( {
|
|
|
414
414
|
} ) => {
|
|
415
415
|
const [ attributes, setAttributes ] = useState< {
|
|
416
416
|
height?: string;
|
|
417
|
-
scale?:
|
|
417
|
+
scale?: number | string;
|
|
418
418
|
} >( {} );
|
|
419
419
|
const { height, scale } = attributes;
|
|
420
420
|
|
|
@@ -512,7 +512,7 @@ export const WithConditionallyRenderedControl: StoryFn<
|
|
|
512
512
|
> = ( { resetAll: resetAllProp, panelId, ...props } ) => {
|
|
513
513
|
const [ attributes, setAttributes ] = useState< {
|
|
514
514
|
height?: string;
|
|
515
|
-
scale?:
|
|
515
|
+
scale?: number | string;
|
|
516
516
|
} >( {} );
|
|
517
517
|
const { height, scale } = attributes;
|
|
518
518
|
|
package/src/tooltip/index.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import { positionToPlacement } from '../popover/utils';
|
|
|
30
30
|
const TooltipInternalContext = createContext< TooltipInternalContextType >( {
|
|
31
31
|
isNestedInTooltip: false,
|
|
32
32
|
} );
|
|
33
|
+
TooltipInternalContext.displayName = 'TooltipInternalContext';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Time over anchor to wait before showing tooltip
|
|
@@ -12,6 +12,8 @@ const RovingTabIndexContext = createContext<
|
|
|
12
12
|
}
|
|
13
13
|
| undefined
|
|
14
14
|
>( undefined );
|
|
15
|
+
RovingTabIndexContext.displayName = 'RovingTabIndexContext';
|
|
16
|
+
|
|
15
17
|
export const useRovingTabIndexContext = () =>
|
|
16
18
|
useContext( RovingTabIndexContext );
|
|
17
19
|
export const RovingTabIndexProvider = RovingTabIndexContext.Provider;
|
package/src/utils/font-size.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { CSSProperties
|
|
4
|
+
import type { CSSProperties } from 'react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
@@ -61,7 +61,7 @@ export function getFontSize(
|
|
|
61
61
|
return `calc(${ ratio } * ${ CONFIG.fontSize })`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function getHeadingFontSize( size:
|
|
64
|
+
export function getHeadingFontSize( size: number | string = 3 ): string {
|
|
65
65
|
if ( ! HEADING_FONT_SIZES.includes( size as HeadingSize ) ) {
|
|
66
66
|
return getFontSize( size );
|
|
67
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { ReactNode,
|
|
4
|
+
import type { ReactNode, ReactElement, ReactPortal } from 'react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies
|
|
@@ -17,7 +17,9 @@ import { Children, isValidElement } from '@wordpress/element';
|
|
|
17
17
|
*/
|
|
18
18
|
export function getValidChildren(
|
|
19
19
|
children: ReactNode
|
|
20
|
-
): Array<
|
|
20
|
+
): Array<
|
|
21
|
+
ReactElement | number | string | Iterable< ReactNode > | ReactPortal
|
|
22
|
+
> {
|
|
21
23
|
if ( typeof children === 'string' ) {
|
|
22
24
|
return [ children ];
|
|
23
25
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* WordPress dependencies
|
|
8
9
|
*/
|
|
9
|
-
import
|
|
10
|
+
import { useRef, useCallback, useState } from '@wordpress/element';
|
|
11
|
+
import { debounce } from '@wordpress/compose';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Internal dependencies
|
|
@@ -14,7 +16,6 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
14
16
|
import { ValidatedInputControl } from '..';
|
|
15
17
|
import { formDecorator } from './story-utils';
|
|
16
18
|
import type { ControlWithError } from '../../control-with-error';
|
|
17
|
-
import { debounce } from '@wordpress/compose';
|
|
18
19
|
|
|
19
20
|
const meta: Meta< typeof ControlWithError > = {
|
|
20
21
|
title: 'Components/Selection & Input/Validated Form Controls/Overview',
|
|
@@ -166,24 +167,19 @@ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = {
|
|
|
166
167
|
} );
|
|
167
168
|
|
|
168
169
|
clearTimeout( timeoutRef.current );
|
|
169
|
-
timeoutRef.current = setTimeout(
|
|
170
|
-
()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
// Mimics a random server response time.
|
|
184
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
185
|
-
Math.random() < 0.5 ? 1500 : 300
|
|
186
|
-
);
|
|
170
|
+
timeoutRef.current = setTimeout( () => {
|
|
171
|
+
if ( v?.toString().toLowerCase() === 'error' ) {
|
|
172
|
+
setCustomValidity( {
|
|
173
|
+
type: 'invalid',
|
|
174
|
+
message: 'The word "error" is not allowed.',
|
|
175
|
+
} );
|
|
176
|
+
} else {
|
|
177
|
+
setCustomValidity( {
|
|
178
|
+
type: 'valid',
|
|
179
|
+
message: 'Validated',
|
|
180
|
+
} );
|
|
181
|
+
}
|
|
182
|
+
}, 1500 );
|
|
187
183
|
}, 500 ),
|
|
188
184
|
[]
|
|
189
185
|
);
|
|
@@ -200,9 +196,95 @@ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = {
|
|
|
200
196
|
/>
|
|
201
197
|
);
|
|
202
198
|
},
|
|
199
|
+
args: {
|
|
200
|
+
label: 'Text',
|
|
201
|
+
help: 'The word "error" will trigger an error asynchronously.',
|
|
202
|
+
required: true,
|
|
203
|
+
},
|
|
203
204
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
|
|
206
|
+
// Not exported - Only for testing purposes.
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
208
|
+
const AsyncValidationWithTest: StoryObj< typeof ValidatedInputControl > = {
|
|
209
|
+
...AsyncValidation,
|
|
210
|
+
play: async ( { canvasElement } ) => {
|
|
211
|
+
const canvas = within( canvasElement );
|
|
212
|
+
await userEvent.click( canvas.getByRole( 'textbox' ) );
|
|
213
|
+
await userEvent.type( canvas.getByRole( 'textbox' ), 'valid text', {
|
|
214
|
+
delay: 10,
|
|
215
|
+
} );
|
|
216
|
+
await userEvent.tab();
|
|
217
|
+
|
|
218
|
+
await waitFor(
|
|
219
|
+
() => {
|
|
220
|
+
expect( canvas.getByText( 'Validated' ) ).toBeVisible();
|
|
221
|
+
},
|
|
222
|
+
{ timeout: 2500 }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 500 ) );
|
|
226
|
+
await userEvent.clear( canvas.getByRole( 'textbox' ) );
|
|
227
|
+
|
|
228
|
+
// Should show validating state when transitioning from valid to invalid.
|
|
229
|
+
await waitFor(
|
|
230
|
+
() => {
|
|
231
|
+
expect( canvas.getByText( 'Validating...' ) ).toBeVisible();
|
|
232
|
+
},
|
|
233
|
+
{ timeout: 2500 }
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await waitFor(
|
|
237
|
+
() => {
|
|
238
|
+
expect(
|
|
239
|
+
canvas.getByText( 'Please fill out this field.' )
|
|
240
|
+
).toBeVisible();
|
|
241
|
+
},
|
|
242
|
+
{ timeout: 2500 }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Should not show validating state if there were no changes
|
|
246
|
+
// after a valid/invalid state was already shown.
|
|
247
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 1500 ) );
|
|
248
|
+
await expect(
|
|
249
|
+
canvas.queryByText( 'Validating...' )
|
|
250
|
+
).not.toBeInTheDocument();
|
|
251
|
+
|
|
252
|
+
await userEvent.type( canvas.getByRole( 'textbox' ), 'e', {
|
|
253
|
+
delay: 10,
|
|
254
|
+
} );
|
|
255
|
+
|
|
256
|
+
// Should not show valid state if server has not yet responded.
|
|
257
|
+
await expect(
|
|
258
|
+
canvas.queryByText( 'Validated' )
|
|
259
|
+
).not.toBeInTheDocument();
|
|
260
|
+
|
|
261
|
+
// Should show validating state when transitioning from invalid to valid.
|
|
262
|
+
await waitFor(
|
|
263
|
+
() => {
|
|
264
|
+
expect( canvas.getByText( 'Validating...' ) ).toBeVisible();
|
|
265
|
+
},
|
|
266
|
+
{ timeout: 2500 }
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await waitFor(
|
|
270
|
+
() => {
|
|
271
|
+
expect( canvas.getByText( 'Validated' ) ).toBeVisible();
|
|
272
|
+
},
|
|
273
|
+
{ timeout: 2500 }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
|
|
277
|
+
await userEvent.type( canvas.getByRole( 'textbox' ), 'rror', {
|
|
278
|
+
delay: 10,
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
await waitFor(
|
|
282
|
+
() => {
|
|
283
|
+
expect(
|
|
284
|
+
canvas.getByText( 'The word "error" is not allowed.' )
|
|
285
|
+
).toBeVisible();
|
|
286
|
+
},
|
|
287
|
+
{ timeout: 2500 }
|
|
288
|
+
);
|
|
289
|
+
},
|
|
208
290
|
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
+
import { usePrevious } from '@wordpress/compose';
|
|
4
5
|
import { __ } from '@wordpress/i18n';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* External dependencies
|
|
8
|
-
*/
|
|
9
6
|
import {
|
|
10
7
|
cloneElement,
|
|
11
8
|
forwardRef,
|
|
@@ -98,6 +95,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
98
95
|
| undefined
|
|
99
96
|
>();
|
|
100
97
|
const [ isTouched, setIsTouched ] = useState( false );
|
|
98
|
+
const previousCustomValidityType = usePrevious( customValidity?.type );
|
|
101
99
|
|
|
102
100
|
// Ensure that error messages are visible after user attemps to submit a form
|
|
103
101
|
// with multiple invalid fields.
|
|
@@ -116,7 +114,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
116
114
|
};
|
|
117
115
|
} );
|
|
118
116
|
|
|
119
|
-
useEffect( () => {
|
|
117
|
+
useEffect( (): ReturnType< React.EffectCallback > => {
|
|
120
118
|
if ( ! isTouched ) {
|
|
121
119
|
return;
|
|
122
120
|
}
|
|
@@ -134,6 +132,9 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
134
132
|
case 'validating': {
|
|
135
133
|
// Wait before showing a validating state.
|
|
136
134
|
const timer = setTimeout( () => {
|
|
135
|
+
validityTarget?.setCustomValidity( '' );
|
|
136
|
+
setErrorMessage( undefined );
|
|
137
|
+
|
|
137
138
|
setStatusMessage( {
|
|
138
139
|
type: 'validating',
|
|
139
140
|
message: customValidity.message,
|
|
@@ -143,6 +144,12 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
143
144
|
return () => clearTimeout( timer );
|
|
144
145
|
}
|
|
145
146
|
case 'valid': {
|
|
147
|
+
// Ensures that we wait for any async responses before showing
|
|
148
|
+
// a synchronously valid state.
|
|
149
|
+
if ( previousCustomValidityType === 'valid' ) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
validityTarget?.setCustomValidity( '' );
|
|
147
154
|
setErrorMessage( validityTarget?.validationMessage );
|
|
148
155
|
|
|
@@ -150,7 +157,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
150
157
|
type: 'valid',
|
|
151
158
|
message: customValidity.message,
|
|
152
159
|
} );
|
|
153
|
-
|
|
160
|
+
break;
|
|
154
161
|
}
|
|
155
162
|
case 'invalid': {
|
|
156
163
|
validityTarget?.setCustomValidity(
|
|
@@ -159,7 +166,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
159
166
|
setErrorMessage( validityTarget?.validationMessage );
|
|
160
167
|
|
|
161
168
|
setStatusMessage( undefined );
|
|
162
|
-
|
|
169
|
+
break;
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
}, [
|
|
@@ -167,9 +174,14 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
167
174
|
customValidity?.type,
|
|
168
175
|
customValidity?.message,
|
|
169
176
|
getValidityTarget,
|
|
177
|
+
previousCustomValidityType,
|
|
170
178
|
] );
|
|
171
179
|
|
|
172
180
|
const onBlur = ( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
181
|
+
if ( isTouched ) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
173
185
|
// Only consider "blurred from the component" if focus has fully left the wrapping div.
|
|
174
186
|
// This prevents unnecessary blurs from components with multiple focusable elements.
|
|
175
187
|
if (
|
|
@@ -177,17 +189,6 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
177
189
|
! event.currentTarget.contains( event.relatedTarget )
|
|
178
190
|
) {
|
|
179
191
|
setIsTouched( true );
|
|
180
|
-
|
|
181
|
-
const validityTarget = getValidityTarget();
|
|
182
|
-
|
|
183
|
-
// Prevents a double flash of the native error tooltip when the control is already showing one.
|
|
184
|
-
if ( ! validityTarget?.validity.valid ) {
|
|
185
|
-
if ( ! errorMessage ) {
|
|
186
|
-
setErrorMessage( validityTarget?.validationMessage );
|
|
187
|
-
}
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
192
|
onValidate?.();
|
|
192
193
|
}
|
|
193
194
|
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WordPress dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useCallback } from '@wordpress/element';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies
|
|
14
|
+
*/
|
|
15
|
+
import { ValidatedInputControl } from '../components';
|
|
16
|
+
|
|
17
|
+
describe( 'ControlWithError', () => {
|
|
18
|
+
describe( 'Async Validation', () => {
|
|
19
|
+
beforeEach( () => {
|
|
20
|
+
jest.useFakeTimers();
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
afterEach( () => {
|
|
24
|
+
jest.useRealTimers();
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
const AsyncValidatedInputControl = ( {
|
|
28
|
+
serverDelayMs,
|
|
29
|
+
...restProps
|
|
30
|
+
}: {
|
|
31
|
+
serverDelayMs: number;
|
|
32
|
+
} & React.ComponentProps< typeof ValidatedInputControl > ) => {
|
|
33
|
+
const [ text, setText ] = useState( '' );
|
|
34
|
+
const [ customValidity, setCustomValidity ] =
|
|
35
|
+
useState<
|
|
36
|
+
React.ComponentProps<
|
|
37
|
+
typeof ValidatedInputControl
|
|
38
|
+
>[ 'customValidity' ]
|
|
39
|
+
>( undefined );
|
|
40
|
+
|
|
41
|
+
const onValidate = useCallback(
|
|
42
|
+
( value?: string ) => {
|
|
43
|
+
setCustomValidity( {
|
|
44
|
+
type: 'validating',
|
|
45
|
+
message: 'Validating...',
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
// Simulate delayed server response
|
|
49
|
+
setTimeout( () => {
|
|
50
|
+
if ( value?.toLowerCase() === 'error' ) {
|
|
51
|
+
setCustomValidity( {
|
|
52
|
+
type: 'invalid',
|
|
53
|
+
message: 'The word "error" is not allowed.',
|
|
54
|
+
} );
|
|
55
|
+
} else {
|
|
56
|
+
setCustomValidity( {
|
|
57
|
+
type: 'valid',
|
|
58
|
+
message: 'Validated',
|
|
59
|
+
} );
|
|
60
|
+
}
|
|
61
|
+
}, serverDelayMs );
|
|
62
|
+
},
|
|
63
|
+
[ serverDelayMs ]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ValidatedInputControl
|
|
68
|
+
label="Text"
|
|
69
|
+
value={ text }
|
|
70
|
+
onChange={ ( newValue ) => {
|
|
71
|
+
setText( newValue ?? '' );
|
|
72
|
+
} }
|
|
73
|
+
onValidate={ onValidate }
|
|
74
|
+
customValidity={ customValidity }
|
|
75
|
+
{ ...restProps }
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
it( 'should not show "validating" state if it takes less than 1000ms', async () => {
|
|
81
|
+
const user = userEvent.setup( {
|
|
82
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
83
|
+
} );
|
|
84
|
+
render( <AsyncValidatedInputControl serverDelayMs={ 500 } /> );
|
|
85
|
+
|
|
86
|
+
const input = screen.getByRole( 'textbox' );
|
|
87
|
+
|
|
88
|
+
await user.type( input, 'valid text' );
|
|
89
|
+
|
|
90
|
+
// Blur to trigger validation
|
|
91
|
+
await user.tab();
|
|
92
|
+
|
|
93
|
+
// Fast-forward to right before the server response
|
|
94
|
+
act( () => jest.advanceTimersByTime( 499 ) );
|
|
95
|
+
|
|
96
|
+
// The validating state should not be shown
|
|
97
|
+
await waitFor( () => {
|
|
98
|
+
expect(
|
|
99
|
+
screen.queryByText( 'Validating...' )
|
|
100
|
+
).not.toBeInTheDocument();
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
// Fast-forward past the server delay to show validation result
|
|
104
|
+
act( () => jest.advanceTimersByTime( 1 ) );
|
|
105
|
+
|
|
106
|
+
await waitFor( () => {
|
|
107
|
+
expect( screen.getByText( 'Validated' ) ).toBeVisible();
|
|
108
|
+
} );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'should show "validating" state if it takes more than 1000ms', async () => {
|
|
112
|
+
const user = userEvent.setup( {
|
|
113
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
114
|
+
} );
|
|
115
|
+
render( <AsyncValidatedInputControl serverDelayMs={ 1200 } /> );
|
|
116
|
+
|
|
117
|
+
const input = screen.getByRole( 'textbox' );
|
|
118
|
+
|
|
119
|
+
await user.type( input, 'valid text' );
|
|
120
|
+
|
|
121
|
+
// Blur to trigger validation
|
|
122
|
+
await user.tab();
|
|
123
|
+
|
|
124
|
+
// Initially, no validating message should be shown (before 1s delay)
|
|
125
|
+
expect(
|
|
126
|
+
screen.queryByText( 'Validating...' )
|
|
127
|
+
).not.toBeInTheDocument();
|
|
128
|
+
|
|
129
|
+
// Fast-forward past the 1s delay to show validating state
|
|
130
|
+
act( () => jest.advanceTimersByTime( 1000 ) );
|
|
131
|
+
|
|
132
|
+
await waitFor( () => {
|
|
133
|
+
expect( screen.getByText( 'Validating...' ) ).toBeVisible();
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
// Fast-forward past the server delay to show validation result
|
|
137
|
+
act( () => jest.advanceTimersByTime( 200 ) );
|
|
138
|
+
|
|
139
|
+
await waitFor( () => {
|
|
140
|
+
expect( screen.getByText( 'Validated' ) ).toBeVisible();
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
// Test error case
|
|
144
|
+
await user.clear( input );
|
|
145
|
+
await user.type( input, 'error' );
|
|
146
|
+
|
|
147
|
+
// Blur to trigger validation
|
|
148
|
+
await user.tab();
|
|
149
|
+
|
|
150
|
+
act( () => jest.advanceTimersByTime( 1000 ) );
|
|
151
|
+
|
|
152
|
+
await waitFor( () => {
|
|
153
|
+
expect( screen.getByText( 'Validating...' ) ).toBeVisible();
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
act( () => jest.advanceTimersByTime( 200 ) );
|
|
157
|
+
|
|
158
|
+
await waitFor( () => {
|
|
159
|
+
expect(
|
|
160
|
+
screen.getByText( 'The word "error" is not allowed.' )
|
|
161
|
+
).toBeVisible();
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
// Test editing after error
|
|
165
|
+
await user.type( input, '{backspace}' );
|
|
166
|
+
|
|
167
|
+
act( () => jest.advanceTimersByTime( 1000 ) );
|
|
168
|
+
|
|
169
|
+
await waitFor( () => {
|
|
170
|
+
expect( screen.getByText( 'Validating...' ) ).toBeVisible();
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
act( () => jest.advanceTimersByTime( 200 ) );
|
|
174
|
+
|
|
175
|
+
await waitFor( () => {
|
|
176
|
+
expect( screen.getByText( 'Validated' ) ).toBeVisible();
|
|
177
|
+
} );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
it( 'should not show a "valid" state until the server response is received, even if locally valid', async () => {
|
|
181
|
+
const user = userEvent.setup( {
|
|
182
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
183
|
+
} );
|
|
184
|
+
render(
|
|
185
|
+
<AsyncValidatedInputControl serverDelayMs={ 1200 } required />
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const input = screen.getByRole( 'textbox' );
|
|
189
|
+
|
|
190
|
+
await user.type( input, 'valid text' );
|
|
191
|
+
|
|
192
|
+
await user.tab();
|
|
193
|
+
act( () => jest.advanceTimersByTime( 1200 ) );
|
|
194
|
+
|
|
195
|
+
await waitFor( () => {
|
|
196
|
+
expect( screen.getByText( 'Validated' ) ).toBeVisible();
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
await user.clear( input );
|
|
200
|
+
|
|
201
|
+
act( () => jest.advanceTimersByTime( 1000 ) );
|
|
202
|
+
|
|
203
|
+
await waitFor( () => {
|
|
204
|
+
expect(
|
|
205
|
+
screen.getByText( 'Constraints not satisfied' )
|
|
206
|
+
).toBeVisible();
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
await user.type( input, 'error' );
|
|
210
|
+
|
|
211
|
+
act( () => jest.advanceTimersByTime( 200 ) );
|
|
212
|
+
|
|
213
|
+
expect( screen.queryByText( 'Validated' ) ).not.toBeInTheDocument();
|
|
214
|
+
|
|
215
|
+
act( () => jest.advanceTimersByTime( 1000 ) );
|
|
216
|
+
|
|
217
|
+
await waitFor( () => {
|
|
218
|
+
expect(
|
|
219
|
+
screen.getByText( 'The word "error" is not allowed.' )
|
|
220
|
+
).toBeVisible();
|
|
221
|
+
} );
|
|
222
|
+
} );
|
|
223
|
+
} );
|
|
224
|
+
} );
|