@wordpress/components 32.4.1-next.v.202603102151.0 → 32.5.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 +19 -1
- package/build/alignment-matrix-control/cell.cjs +2 -2
- package/build/alignment-matrix-control/cell.cjs.map +1 -1
- package/build/alignment-matrix-control/index.cjs +2 -2
- package/build/alignment-matrix-control/index.cjs.map +1 -1
- package/build/angle-picker-control/angle-circle.cjs +2 -2
- package/build/angle-picker-control/angle-circle.cjs.map +1 -1
- package/build/combobox-control/index.cjs +5 -1
- package/build/combobox-control/index.cjs.map +2 -2
- package/build/custom-gradient-picker/index.cjs +9 -1
- package/build/custom-gradient-picker/index.cjs.map +2 -2
- package/build/date-time/time/index.cjs +1 -1
- package/build/date-time/time/index.cjs.map +2 -2
- package/build/date-time/utils.cjs +9 -0
- package/build/date-time/utils.cjs.map +2 -2
- package/build/form-token-field/token-input.cjs +2 -1
- package/build/form-token-field/token-input.cjs.map +2 -2
- package/build/radio-control/index.cjs +1 -0
- package/build/radio-control/index.cjs.map +2 -2
- package/build/toggle-group-control/toggle-group-control/as-button-group.cjs +1 -0
- package/build/toggle-group-control/toggle-group-control/as-button-group.cjs.map +2 -2
- package/build/toggle-group-control/toggle-group-control/component.cjs +15 -9
- package/build/toggle-group-control/toggle-group-control/component.cjs.map +3 -3
- package/build/toggle-group-control/toggle-group-control/styles.cjs +6 -32
- package/build/toggle-group-control/toggle-group-control/styles.cjs.map +3 -3
- package/build/validated-form-controls/control-with-error.cjs +26 -3
- package/build/validated-form-controls/control-with-error.cjs.map +2 -2
- package/build/validated-form-controls/validity-indicator.cjs +2 -0
- package/build/validated-form-controls/validity-indicator.cjs.map +2 -2
- package/build-module/alignment-matrix-control/cell.mjs +2 -2
- package/build-module/alignment-matrix-control/cell.mjs.map +1 -1
- package/build-module/alignment-matrix-control/index.mjs +2 -2
- package/build-module/alignment-matrix-control/index.mjs.map +1 -1
- package/build-module/angle-picker-control/angle-circle.mjs +2 -2
- package/build-module/angle-picker-control/angle-circle.mjs.map +1 -1
- package/build-module/combobox-control/index.mjs +5 -1
- package/build-module/combobox-control/index.mjs.map +2 -2
- package/build-module/custom-gradient-picker/index.mjs +10 -2
- package/build-module/custom-gradient-picker/index.mjs.map +2 -2
- package/build-module/date-time/time/index.mjs +2 -2
- package/build-module/date-time/time/index.mjs.map +2 -2
- package/build-module/date-time/utils.mjs +8 -0
- package/build-module/date-time/utils.mjs.map +2 -2
- package/build-module/form-token-field/token-input.mjs +2 -1
- package/build-module/form-token-field/token-input.mjs.map +2 -2
- package/build-module/radio-control/index.mjs +1 -0
- package/build-module/radio-control/index.mjs.map +2 -2
- package/build-module/toggle-group-control/toggle-group-control/as-button-group.mjs +1 -0
- package/build-module/toggle-group-control/toggle-group-control/as-button-group.mjs.map +2 -2
- package/build-module/toggle-group-control/toggle-group-control/component.mjs +17 -11
- package/build-module/toggle-group-control/toggle-group-control/component.mjs.map +2 -2
- package/build-module/toggle-group-control/toggle-group-control/styles.mjs +6 -21
- package/build-module/toggle-group-control/toggle-group-control/styles.mjs.map +2 -2
- package/build-module/validated-form-controls/control-with-error.mjs +27 -4
- package/build-module/validated-form-controls/control-with-error.mjs.map +2 -2
- package/build-module/validated-form-controls/validity-indicator.mjs +2 -0
- package/build-module/validated-form-controls/validity-indicator.mjs.map +2 -2
- package/build-style/style-rtl.css +14 -8
- package/build-style/style.css +14 -8
- package/build-types/alignment-matrix-control/stories/index.story.d.ts +1 -1
- package/build-types/alignment-matrix-control/stories/index.story.d.ts.map +1 -1
- package/build-types/angle-picker-control/stories/index.story.d.ts +1 -1
- package/build-types/animate/stories/index.story.d.ts +7 -7
- package/build-types/animate/stories/index.story.d.ts.map +1 -1
- package/build-types/base-control/stories/index.story.d.ts +1 -1
- package/build-types/border-box-control/stories/index.story.d.ts +1 -1
- package/build-types/border-control/stories/index.story.d.ts +5 -5
- package/build-types/box-control/stories/index.story.d.ts +7 -7
- package/build-types/box-control/stories/index.story.d.ts.map +1 -1
- package/build-types/button/stories/e2e/index.story.d.ts +1 -1
- package/build-types/button/stories/e2e/index.story.d.ts.map +1 -1
- package/build-types/button/stories/index.story.d.ts +7 -7
- package/build-types/button/stories/index.story.d.ts.map +1 -1
- package/build-types/circular-option-picker/stories/index.story.d.ts +5 -5
- package/build-types/circular-option-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/combobox-control/index.d.ts.map +1 -1
- package/build-types/combobox-control/stories/index.story.d.ts +4 -4
- package/build-types/combobox-control/stories/index.story.d.ts.map +1 -1
- package/build-types/confirm-dialog/stories/index.story.d.ts +2 -2
- package/build-types/confirm-dialog/stories/index.story.d.ts.map +1 -1
- package/build-types/custom-gradient-picker/index.d.ts.map +1 -1
- package/build-types/custom-gradient-picker/stories/index.story.d.ts +1 -1
- package/build-types/custom-gradient-picker/stories/index.story.d.ts.map +1 -1
- package/build-types/custom-gradient-picker/test/index.d.ts +2 -0
- package/build-types/custom-gradient-picker/test/index.d.ts.map +1 -0
- package/build-types/custom-select-control/stories/index.story.d.ts +3 -3
- package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
- package/build-types/custom-select-control-v2/stories/index.story.d.ts +3 -3
- package/build-types/date-time/stories/time.story.d.ts +1 -1
- package/build-types/date-time/stories/time.story.d.ts.map +1 -1
- package/build-types/date-time/time/index.d.ts.map +1 -1
- package/build-types/date-time/utils.d.ts +9 -0
- package/build-types/date-time/utils.d.ts.map +1 -1
- package/build-types/drop-zone/stories/index.story.d.ts +1 -1
- package/build-types/drop-zone/stories/index.story.d.ts.map +1 -1
- package/build-types/duotone-picker/stories/duotone-picker.story.d.ts +1 -1
- package/build-types/duotone-picker/stories/duotone-picker.story.d.ts.map +1 -1
- package/build-types/duotone-picker/stories/duotone-swatch.story.d.ts +3 -3
- package/build-types/duotone-picker/stories/duotone-swatch.story.d.ts.map +1 -1
- package/build-types/focal-point-picker/stories/index.story.d.ts +4 -4
- package/build-types/form-file-upload/stories/index.story.d.ts +5 -5
- package/build-types/form-file-upload/stories/index.story.d.ts.map +1 -1
- package/build-types/form-token-field/token-input.d.ts.map +1 -1
- package/build-types/guide/stories/index.story.d.ts +1 -1
- package/build-types/guide/stories/index.story.d.ts.map +1 -1
- package/build-types/icon/stories/index.story.d.ts +4 -4
- package/build-types/icon/stories/index.story.d.ts.map +1 -1
- package/build-types/input-control/stories/index.story.d.ts +7 -7
- package/build-types/input-control/stories/index.story.d.ts.map +1 -1
- package/build-types/keyboard-shortcuts/stories/index.story.d.ts +1 -1
- package/build-types/keyboard-shortcuts/stories/index.story.d.ts.map +1 -1
- package/build-types/menu-group/stories/index.story.d.ts +1 -1
- package/build-types/menu-group/stories/index.story.d.ts.map +1 -1
- package/build-types/menu-item/stories/index.story.d.ts +4 -4
- package/build-types/navigation/stories/index.story.d.ts +6 -6
- package/build-types/navigation/stories/index.story.d.ts.map +1 -1
- package/build-types/notice/stories/index.story.d.ts +5 -5
- package/build-types/notice/stories/index.story.d.ts.map +1 -1
- package/build-types/number-control/stories/index.story.d.ts +1 -1
- package/build-types/palette-edit/stories/index.story.d.ts +2 -2
- package/build-types/palette-edit/stories/index.story.d.ts.map +1 -1
- package/build-types/progress-bar/stories/index.story.d.ts +1 -1
- package/build-types/progress-bar/stories/index.story.d.ts.map +1 -1
- package/build-types/query-controls/stories/index.story.d.ts +1 -1
- package/build-types/query-controls/stories/index.story.d.ts.map +1 -1
- package/build-types/radio-control/index.d.ts.map +1 -1
- package/build-types/resizable-box/stories/index.story.d.ts +2 -2
- package/build-types/responsive-wrapper/stories/index.story.d.ts +1 -1
- package/build-types/responsive-wrapper/stories/index.story.d.ts.map +1 -1
- package/build-types/sandbox/stories/index.story.d.ts +1 -1
- package/build-types/sandbox/stories/index.story.d.ts.map +1 -1
- package/build-types/search-control/stories/index.story.d.ts +1 -1
- package/build-types/select-control/stories/index.story.d.ts +5 -5
- package/build-types/shortcut/stories/index.story.d.ts +1 -1
- package/build-types/shortcut/stories/index.story.d.ts.map +1 -1
- package/build-types/tab-panel/stories/index.story.d.ts +4 -4
- package/build-types/tab-panel/stories/index.story.d.ts.map +1 -1
- package/build-types/tabs/stories/index.story.d.ts +7 -7
- package/build-types/tabs/stories/index.story.d.ts.map +1 -1
- package/build-types/text/stories/index.story.d.ts +3 -3
- package/build-types/theme/stories/index.story.d.ts +1 -1
- package/build-types/toggle-control/stories/index.story.d.ts +2 -2
- package/build-types/toggle-group-control/stories/index.story.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/as-button-group.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/component.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/styles.d.ts +0 -4
- package/build-types/toggle-group-control/toggle-group-control/styles.d.ts.map +1 -1
- package/build-types/toolbar/stories/index.story.d.ts +3 -3
- package/build-types/toolbar/stories/index.story.d.ts.map +1 -1
- package/build-types/tooltip/stories/index.story.d.ts +1 -1
- package/build-types/tooltip/stories/index.story.d.ts.map +1 -1
- package/build-types/tree-grid/stories/index.story.d.ts +1 -1
- package/build-types/tree-grid/stories/index.story.d.ts.map +1 -1
- package/build-types/tree-select/stories/index.story.d.ts +1 -1
- package/build-types/tree-select/stories/index.story.d.ts.map +1 -1
- package/build-types/v-stack/stories/index.story.d.ts +1 -1
- package/build-types/validated-form-controls/control-with-error.d.ts.map +1 -1
- package/build-types/validated-form-controls/test/checkbox-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/checkbox-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/combobox-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/combobox-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/custom-select-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/custom-select-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/form-token-field.d.ts +2 -0
- package/build-types/validated-form-controls/test/form-token-field.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/input-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/input-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/number-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/number-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/radio-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/radio-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/range-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/range-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/select-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/select-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/text-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/text-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/textarea-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/textarea-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/toggle-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/toggle-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/test/toggle-group-control.d.ts +2 -0
- package/build-types/validated-form-controls/test/toggle-group-control.d.ts.map +1 -0
- package/build-types/validated-form-controls/validity-indicator.d.ts +2 -1
- package/build-types/validated-form-controls/validity-indicator.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/button/style.scss +16 -5
- package/src/button-group/stories/index.story.tsx +1 -1
- package/src/combobox-control/index.tsx +6 -0
- package/src/combobox-control/stories/index.story.tsx +3 -2
- package/src/combobox-control/test/index.tsx +16 -9
- package/src/composite/legacy/stories/index.story.tsx +1 -1
- package/src/custom-gradient-picker/index.tsx +15 -4
- package/src/custom-gradient-picker/test/index.tsx +81 -0
- package/src/date-time/test/utils.test.ts +8 -11
- package/src/date-time/time/index.tsx +2 -12
- package/src/date-time/time/test/index.tsx +69 -0
- package/src/date-time/utils.ts +18 -0
- package/src/form-token-field/token-input.tsx +7 -1
- package/src/guide/style.scss +3 -0
- package/src/modal/style.scss +4 -7
- package/src/navigation/stories/index.story.tsx +1 -1
- package/src/radio-control/index.tsx +1 -0
- package/src/radio-control/test/index.tsx +5 -5
- package/src/radio-group/stories/index.story.tsx +1 -1
- package/src/snackbar/style.scss +1 -1
- package/src/toggle-group-control/stories/index.story.tsx +1 -0
- package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +124 -164
- package/src/toggle-group-control/test/index.tsx +54 -0
- package/src/toggle-group-control/toggle-group-control/as-button-group.tsx +1 -0
- package/src/toggle-group-control/toggle-group-control/component.tsx +13 -8
- package/src/toggle-group-control/toggle-group-control/styles.ts +0 -6
- package/src/validated-form-controls/control-with-error.tsx +44 -4
- package/src/validated-form-controls/test/checkbox-control.tsx +49 -0
- package/src/validated-form-controls/test/combobox-control.tsx +61 -0
- package/src/validated-form-controls/test/control-with-error.tsx +182 -1
- package/src/validated-form-controls/test/custom-select-control.tsx +60 -0
- package/src/validated-form-controls/test/form-token-field.tsx +52 -0
- package/src/validated-form-controls/test/input-control.tsx +42 -0
- package/src/validated-form-controls/test/number-control.tsx +44 -0
- package/src/validated-form-controls/test/radio-control.tsx +61 -0
- package/src/validated-form-controls/test/range-control.tsx +73 -0
- package/src/validated-form-controls/test/select-control.tsx +57 -0
- package/src/validated-form-controls/test/text-control.tsx +49 -0
- package/src/validated-form-controls/test/textarea-control.tsx +51 -0
- package/src/validated-form-controls/test/toggle-control.tsx +49 -0
- package/src/validated-form-controls/test/toggle-group-control.tsx +28 -0
- package/src/validated-form-controls/validity-indicator.tsx +3 -0
|
@@ -353,6 +353,60 @@ describe.each( [
|
|
|
353
353
|
);
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
it( 'should render the label', () => {
|
|
357
|
+
render(
|
|
358
|
+
<Component label="Test Toggle Group Control">{ options }</Component>
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect( screen.getByText( 'Test Toggle Group Control' ) ).toBeVisible();
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
it( 'should still label the control accessibly when hideLabelFromVision is true', () => {
|
|
365
|
+
render(
|
|
366
|
+
<Component label="Test Toggle Group Control" hideLabelFromVision>
|
|
367
|
+
{ options }
|
|
368
|
+
</Component>
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(
|
|
372
|
+
screen.getByRole( 'radiogroup', {
|
|
373
|
+
name: 'Test Toggle Group Control',
|
|
374
|
+
} )
|
|
375
|
+
).toBeVisible();
|
|
376
|
+
} );
|
|
377
|
+
|
|
378
|
+
it( 'should accessibly associate the help text', () => {
|
|
379
|
+
render(
|
|
380
|
+
<Component label="Test Toggle Group Control" help="Help text">
|
|
381
|
+
{ options }
|
|
382
|
+
</Component>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(
|
|
386
|
+
screen.getByRole( 'radiogroup', {
|
|
387
|
+
description: 'Help text',
|
|
388
|
+
} )
|
|
389
|
+
).toBeVisible();
|
|
390
|
+
} );
|
|
391
|
+
|
|
392
|
+
it( 'should accessibly associate the help text when isDeselectable', () => {
|
|
393
|
+
render(
|
|
394
|
+
<Component
|
|
395
|
+
label="Test Toggle Group Control"
|
|
396
|
+
help="Help text"
|
|
397
|
+
isDeselectable
|
|
398
|
+
>
|
|
399
|
+
{ options }
|
|
400
|
+
</Component>
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(
|
|
404
|
+
screen.getByRole( 'group', {
|
|
405
|
+
description: 'Help text',
|
|
406
|
+
} )
|
|
407
|
+
).toBeVisible();
|
|
408
|
+
} );
|
|
409
|
+
|
|
356
410
|
describe( 'isDeselectable', () => {
|
|
357
411
|
describe( 'isDeselectable = false', () => {
|
|
358
412
|
it( 'should not be deselectable', async () => {
|
|
@@ -15,9 +15,8 @@ import { useMergeRefs } from '@wordpress/compose';
|
|
|
15
15
|
import type { WordPressComponentProps } from '../../context';
|
|
16
16
|
import { contextConnect, useContextSystem } from '../../context';
|
|
17
17
|
import { useCx } from '../../utils/hooks';
|
|
18
|
-
import BaseControl from '../../base-control';
|
|
18
|
+
import BaseControl, { useBaseControlProps } from '../../base-control';
|
|
19
19
|
import type { ToggleGroupControlProps } from '../types';
|
|
20
|
-
import { VisualLabelWrapper } from './styles';
|
|
21
20
|
import * as styles from './styles';
|
|
22
21
|
import { ToggleGroupControlAsRadioGroup } from './as-radio-group';
|
|
23
22
|
import { ToggleGroupControlAsButtonGroup } from './as-button-group';
|
|
@@ -37,6 +36,7 @@ function UnconnectedToggleGroupControl(
|
|
|
37
36
|
isAdaptiveWidth = false,
|
|
38
37
|
isBlock = false,
|
|
39
38
|
isDeselectable = false,
|
|
39
|
+
id,
|
|
40
40
|
label,
|
|
41
41
|
hideLabelFromVision = false,
|
|
42
42
|
help,
|
|
@@ -47,6 +47,13 @@ function UnconnectedToggleGroupControl(
|
|
|
47
47
|
...otherProps
|
|
48
48
|
} = useContextSystem( props, 'ToggleGroupControl' );
|
|
49
49
|
|
|
50
|
+
const { baseControlProps, controlProps } = useBaseControlProps( {
|
|
51
|
+
id,
|
|
52
|
+
help,
|
|
53
|
+
label,
|
|
54
|
+
hideLabelFromVision,
|
|
55
|
+
} );
|
|
56
|
+
|
|
50
57
|
const normalizedSize =
|
|
51
58
|
__next40pxDefaultSize && size === 'default' ? '__unstable-large' : size;
|
|
52
59
|
|
|
@@ -91,17 +98,15 @@ function UnconnectedToggleGroupControl(
|
|
|
91
98
|
} );
|
|
92
99
|
|
|
93
100
|
return (
|
|
94
|
-
<BaseControl
|
|
95
|
-
{ ! hideLabelFromVision && (
|
|
96
|
-
<VisualLabelWrapper>
|
|
97
|
-
<BaseControl.VisualLabel>{ label }</BaseControl.VisualLabel>
|
|
98
|
-
</VisualLabelWrapper>
|
|
99
|
-
) }
|
|
101
|
+
<BaseControl { ...baseControlProps }>
|
|
100
102
|
<MainControl
|
|
101
103
|
{ ...otherProps }
|
|
104
|
+
{ ...controlProps }
|
|
102
105
|
setSelectedElement={ setSelectedElement }
|
|
103
106
|
className={ classes }
|
|
104
107
|
isAdaptiveWidth={ isAdaptiveWidth }
|
|
108
|
+
// `label` is used for `aria-label` on the inner control.
|
|
109
|
+
// This is separate from the visual label rendered by `BaseControl`.
|
|
105
110
|
label={ label }
|
|
106
111
|
onChange={ onChange }
|
|
107
112
|
ref={ refs }
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { css } from '@emotion/react';
|
|
5
|
-
import styled from '@emotion/styled';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Internal dependencies
|
|
@@ -100,8 +99,3 @@ export const block = css`
|
|
|
100
99
|
display: flex;
|
|
101
100
|
width: 100%;
|
|
102
101
|
`;
|
|
103
|
-
|
|
104
|
-
export const VisualLabelWrapper = styled.div`
|
|
105
|
-
// Makes the inline label be the correct height, equivalent to setting line-height: 0
|
|
106
|
-
display: flex;
|
|
107
|
-
`;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
cloneElement,
|
|
7
7
|
forwardRef,
|
|
8
8
|
useEffect,
|
|
9
|
+
useId,
|
|
9
10
|
useState,
|
|
10
11
|
} from '@wordpress/element';
|
|
11
12
|
|
|
@@ -238,22 +239,61 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
238
239
|
}
|
|
239
240
|
};
|
|
240
241
|
|
|
241
|
-
const
|
|
242
|
+
const messageId = useId();
|
|
243
|
+
|
|
244
|
+
const message = ( () => {
|
|
242
245
|
if ( errorMessage ) {
|
|
243
246
|
return (
|
|
244
|
-
<ValidityIndicator
|
|
247
|
+
<ValidityIndicator
|
|
248
|
+
id={ messageId }
|
|
249
|
+
type="invalid"
|
|
250
|
+
message={ errorMessage }
|
|
251
|
+
/>
|
|
245
252
|
);
|
|
246
253
|
}
|
|
247
254
|
if ( statusMessage?.type ) {
|
|
248
255
|
return (
|
|
249
256
|
<ValidityIndicator
|
|
257
|
+
id={ messageId }
|
|
250
258
|
type={ statusMessage.type }
|
|
251
259
|
message={ statusMessage.message }
|
|
252
260
|
/>
|
|
253
261
|
);
|
|
254
262
|
}
|
|
255
263
|
return null;
|
|
256
|
-
};
|
|
264
|
+
} )();
|
|
265
|
+
|
|
266
|
+
const visibleMessage = showMessage ? message : null;
|
|
267
|
+
|
|
268
|
+
// Imperatively manage `aria-describedby` on the validity target so we
|
|
269
|
+
// merge with any value the child control sets internally (e.g. from a
|
|
270
|
+
// `help` prop), rather than competing with it at the props level.
|
|
271
|
+
useEffect( () => {
|
|
272
|
+
const target = getValidityTarget();
|
|
273
|
+
if ( ! target ) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setDescribedBy( el: Element, shouldAdd: boolean ) {
|
|
278
|
+
const ids = ( el.getAttribute( 'aria-describedby' ) ?? '' )
|
|
279
|
+
.split( ' ' )
|
|
280
|
+
.filter( ( id ) => id && id !== messageId );
|
|
281
|
+
|
|
282
|
+
if ( shouldAdd ) {
|
|
283
|
+
ids.push( messageId );
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if ( ids.length ) {
|
|
287
|
+
el.setAttribute( 'aria-describedby', ids.join( ' ' ) );
|
|
288
|
+
} else {
|
|
289
|
+
el.removeAttribute( 'aria-describedby' );
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
setDescribedBy( target, !! visibleMessage );
|
|
294
|
+
|
|
295
|
+
return () => setDescribedBy( target, false );
|
|
296
|
+
}, [ visibleMessage, messageId, getValidityTarget ] );
|
|
257
297
|
|
|
258
298
|
return (
|
|
259
299
|
<div className={ className } ref={ forwardedRef } onBlur={ onBlur }>
|
|
@@ -265,7 +305,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >(
|
|
|
265
305
|
),
|
|
266
306
|
required,
|
|
267
307
|
} ) }
|
|
268
|
-
<div aria-live="polite">{
|
|
308
|
+
<div aria-live="polite">{ visibleMessage }</div>
|
|
269
309
|
</div>
|
|
270
310
|
);
|
|
271
311
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ValidatedCheckboxControl } from '../components';
|
|
4
|
+
|
|
5
|
+
describe( 'ValidatedCheckboxControl', () => {
|
|
6
|
+
it( 'should preserve the help description', () => {
|
|
7
|
+
render(
|
|
8
|
+
<ValidatedCheckboxControl
|
|
9
|
+
label="Agree"
|
|
10
|
+
help="You must agree to continue."
|
|
11
|
+
onChange={ () => {} }
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(
|
|
16
|
+
screen.getByRole( 'checkbox', { name: 'Agree' } )
|
|
17
|
+
).toHaveAccessibleDescription( 'You must agree to continue.' );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'should append the validation error alongside the help description', async () => {
|
|
21
|
+
const user = userEvent.setup();
|
|
22
|
+
render(
|
|
23
|
+
<form>
|
|
24
|
+
<ValidatedCheckboxControl
|
|
25
|
+
label="Agree"
|
|
26
|
+
help="You must agree to continue."
|
|
27
|
+
onChange={ () => {} }
|
|
28
|
+
required
|
|
29
|
+
/>
|
|
30
|
+
<button type="submit">Submit</button>
|
|
31
|
+
</form>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const checkbox = screen.getByRole( 'checkbox', {
|
|
35
|
+
name: /^Agree/,
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
await user.click( screen.getByRole( 'button', { name: 'Submit' } ) );
|
|
39
|
+
|
|
40
|
+
await waitFor( () => {
|
|
41
|
+
expect( checkbox ).toHaveAccessibleDescription(
|
|
42
|
+
expect.stringContaining( 'Constraints not satisfied' )
|
|
43
|
+
);
|
|
44
|
+
} );
|
|
45
|
+
expect( checkbox ).toHaveAccessibleDescription(
|
|
46
|
+
expect.stringContaining( 'You must agree to continue.' )
|
|
47
|
+
);
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ValidatedComboboxControl } from '../components';
|
|
4
|
+
|
|
5
|
+
// The `help` prop is rendered visually by BaseControl but is not
|
|
6
|
+
// programmatically associated with the combobox input via aria-describedby.
|
|
7
|
+
// This is a pre-existing bug in ComboboxControl, not caused by ControlWithError.
|
|
8
|
+
describe( 'ValidatedComboboxControl', () => {
|
|
9
|
+
const options = [
|
|
10
|
+
{ label: 'Apple', value: 'apple' },
|
|
11
|
+
{ label: 'Banana', value: 'banana' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
|
15
|
+
it.skip( 'should preserve the help description', () => {
|
|
16
|
+
render(
|
|
17
|
+
<ValidatedComboboxControl
|
|
18
|
+
label="Fruit"
|
|
19
|
+
help="Pick a fruit."
|
|
20
|
+
options={ options }
|
|
21
|
+
onChange={ () => {} }
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(
|
|
26
|
+
screen.getByRole( 'combobox', { name: 'Fruit' } )
|
|
27
|
+
).toHaveAccessibleDescription( 'Pick a fruit.' );
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
|
31
|
+
it.skip( 'should append the validation error alongside the help description', async () => {
|
|
32
|
+
const user = userEvent.setup();
|
|
33
|
+
render(
|
|
34
|
+
<form>
|
|
35
|
+
<ValidatedComboboxControl
|
|
36
|
+
label="Fruit"
|
|
37
|
+
help="Pick a fruit."
|
|
38
|
+
options={ options }
|
|
39
|
+
onChange={ () => {} }
|
|
40
|
+
required
|
|
41
|
+
/>
|
|
42
|
+
<button type="submit">Submit</button>
|
|
43
|
+
</form>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const combobox = screen.getByRole( 'combobox', {
|
|
47
|
+
name: /^Fruit/,
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
await user.click( screen.getByRole( 'button', { name: 'Submit' } ) );
|
|
51
|
+
|
|
52
|
+
await waitFor( () => {
|
|
53
|
+
expect( combobox ).toHaveAccessibleDescription(
|
|
54
|
+
expect.stringContaining( 'Constraints not satisfied' )
|
|
55
|
+
);
|
|
56
|
+
} );
|
|
57
|
+
expect( combobox ).toHaveAccessibleDescription(
|
|
58
|
+
expect.stringContaining( 'Pick a fruit.' )
|
|
59
|
+
);
|
|
60
|
+
} );
|
|
61
|
+
} );
|
|
@@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
7
7
|
/**
|
|
8
8
|
* WordPress dependencies
|
|
9
9
|
*/
|
|
10
|
-
import { useState, useCallback, useRef } from '@wordpress/element';
|
|
10
|
+
import { useState, useCallback, useId, useRef } from '@wordpress/element';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Internal dependencies
|
|
@@ -283,6 +283,187 @@ describe( 'ControlWithError', () => {
|
|
|
283
283
|
} );
|
|
284
284
|
} );
|
|
285
285
|
|
|
286
|
+
describe( 'aria-describedby', () => {
|
|
287
|
+
it( 'should connect the error message to the input via aria-describedby', async () => {
|
|
288
|
+
const user = userEvent.setup();
|
|
289
|
+
render(
|
|
290
|
+
<form>
|
|
291
|
+
<ValidatedInputControl label="URL" required />
|
|
292
|
+
<button type="submit">Submit</button>
|
|
293
|
+
</form>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const input = screen.getByRole( 'textbox', { name: /^URL/ } );
|
|
297
|
+
|
|
298
|
+
expect( input ).not.toHaveAttribute( 'aria-describedby' );
|
|
299
|
+
|
|
300
|
+
await user.click(
|
|
301
|
+
screen.getByRole( 'button', { name: 'Submit' } )
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await waitFor( () => {
|
|
305
|
+
expect( input ).toHaveAccessibleDescription(
|
|
306
|
+
expect.stringContaining( 'Constraints not satisfied' )
|
|
307
|
+
);
|
|
308
|
+
} );
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
it( 'should preserve existing aria-describedby values', async () => {
|
|
312
|
+
const user = userEvent.setup();
|
|
313
|
+
|
|
314
|
+
function TestComponent() {
|
|
315
|
+
const hintId = useId();
|
|
316
|
+
return (
|
|
317
|
+
<form>
|
|
318
|
+
<ValidatedInputControl
|
|
319
|
+
label="URL"
|
|
320
|
+
required
|
|
321
|
+
aria-describedby={ hintId }
|
|
322
|
+
/>
|
|
323
|
+
<p id={ hintId }>Enter a full URL.</p>
|
|
324
|
+
<button type="submit">Submit</button>
|
|
325
|
+
</form>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
render( <TestComponent /> );
|
|
330
|
+
|
|
331
|
+
const input = screen.getByRole( 'textbox', { name: /^URL/ } );
|
|
332
|
+
|
|
333
|
+
expect( input ).toHaveAccessibleDescription( 'Enter a full URL.' );
|
|
334
|
+
|
|
335
|
+
await user.click(
|
|
336
|
+
screen.getByRole( 'button', { name: 'Submit' } )
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
await waitFor( () => {
|
|
340
|
+
expect( input ).toHaveAccessibleDescription(
|
|
341
|
+
expect.stringContaining( 'Constraints not satisfied' )
|
|
342
|
+
);
|
|
343
|
+
} );
|
|
344
|
+
expect( input ).toHaveAccessibleDescription(
|
|
345
|
+
expect.stringContaining( 'Enter a full URL.' )
|
|
346
|
+
);
|
|
347
|
+
} );
|
|
348
|
+
|
|
349
|
+
it( 'should connect a custom validity error to the input via aria-describedby', async () => {
|
|
350
|
+
const user = userEvent.setup();
|
|
351
|
+
|
|
352
|
+
function TestComponent() {
|
|
353
|
+
const [ customValidity, setCustomValidity ] =
|
|
354
|
+
useState<
|
|
355
|
+
React.ComponentProps<
|
|
356
|
+
typeof ValidatedInputControl
|
|
357
|
+
>[ 'customValidity' ]
|
|
358
|
+
>( undefined );
|
|
359
|
+
const inputRef = useRef< HTMLInputElement >( null );
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<>
|
|
363
|
+
<ValidatedInputControl
|
|
364
|
+
ref={ inputRef }
|
|
365
|
+
label="URL"
|
|
366
|
+
customValidity={ customValidity }
|
|
367
|
+
/>
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={ () => {
|
|
371
|
+
setCustomValidity( {
|
|
372
|
+
type: 'invalid',
|
|
373
|
+
message: 'Please enter a valid URL.',
|
|
374
|
+
} );
|
|
375
|
+
requestAnimationFrame(
|
|
376
|
+
() => inputRef.current?.reportValidity()
|
|
377
|
+
);
|
|
378
|
+
} }
|
|
379
|
+
>
|
|
380
|
+
Validate
|
|
381
|
+
</button>
|
|
382
|
+
</>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
render( <TestComponent /> );
|
|
387
|
+
|
|
388
|
+
const input = screen.getByRole( 'textbox', { name: 'URL' } );
|
|
389
|
+
expect( input ).not.toHaveAttribute( 'aria-describedby' );
|
|
390
|
+
|
|
391
|
+
await user.click(
|
|
392
|
+
screen.getByRole( 'button', { name: 'Validate' } )
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await waitFor( () => {
|
|
396
|
+
expect( input ).toHaveAccessibleDescription(
|
|
397
|
+
expect.stringContaining( 'Please enter a valid URL.' )
|
|
398
|
+
);
|
|
399
|
+
} );
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
it( 'should remove aria-describedby when the error is resolved', async () => {
|
|
403
|
+
const user = userEvent.setup();
|
|
404
|
+
|
|
405
|
+
function TestComponent() {
|
|
406
|
+
const [ customValidity, setCustomValidity ] =
|
|
407
|
+
useState<
|
|
408
|
+
React.ComponentProps<
|
|
409
|
+
typeof ValidatedInputControl
|
|
410
|
+
>[ 'customValidity' ]
|
|
411
|
+
>( undefined );
|
|
412
|
+
const inputRef = useRef< HTMLInputElement >( null );
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<>
|
|
416
|
+
<ValidatedInputControl
|
|
417
|
+
ref={ inputRef }
|
|
418
|
+
label="URL"
|
|
419
|
+
customValidity={ customValidity }
|
|
420
|
+
/>
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
onClick={ () => {
|
|
424
|
+
setCustomValidity( {
|
|
425
|
+
type: 'invalid',
|
|
426
|
+
message: 'Please enter a valid URL.',
|
|
427
|
+
} );
|
|
428
|
+
requestAnimationFrame(
|
|
429
|
+
() => inputRef.current?.reportValidity()
|
|
430
|
+
);
|
|
431
|
+
} }
|
|
432
|
+
>
|
|
433
|
+
Validate
|
|
434
|
+
</button>
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
onClick={ () => setCustomValidity( undefined ) }
|
|
438
|
+
>
|
|
439
|
+
Clear
|
|
440
|
+
</button>
|
|
441
|
+
</>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
render( <TestComponent /> );
|
|
446
|
+
|
|
447
|
+
const input = screen.getByRole( 'textbox', { name: 'URL' } );
|
|
448
|
+
|
|
449
|
+
await user.click(
|
|
450
|
+
screen.getByRole( 'button', { name: 'Validate' } )
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
await waitFor( () => {
|
|
454
|
+
expect( input ).toHaveAccessibleDescription(
|
|
455
|
+
expect.stringContaining( 'Please enter a valid URL.' )
|
|
456
|
+
);
|
|
457
|
+
} );
|
|
458
|
+
|
|
459
|
+
await user.click( screen.getByRole( 'button', { name: 'Clear' } ) );
|
|
460
|
+
|
|
461
|
+
await waitFor( () => {
|
|
462
|
+
expect( input ).not.toHaveAttribute( 'aria-describedby' );
|
|
463
|
+
} );
|
|
464
|
+
} );
|
|
465
|
+
} );
|
|
466
|
+
|
|
286
467
|
describe( 'Focus behavior', () => {
|
|
287
468
|
it( 'should focus the first error in the form', async () => {
|
|
288
469
|
const user = userEvent.setup();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ValidatedCustomSelectControl } from '../components';
|
|
4
|
+
|
|
5
|
+
describe( 'ValidatedCustomSelectControl', () => {
|
|
6
|
+
const options = [
|
|
7
|
+
{ key: 'small', name: 'Small' },
|
|
8
|
+
{ key: 'large', name: 'Large' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
it( 'should preserve the built-in "Currently selected" description', async () => {
|
|
12
|
+
render(
|
|
13
|
+
<ValidatedCustomSelectControl
|
|
14
|
+
label="Font Size"
|
|
15
|
+
options={ options }
|
|
16
|
+
value={ options[ 0 ] }
|
|
17
|
+
onChange={ () => {} }
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await waitFor( () => {
|
|
22
|
+
expect(
|
|
23
|
+
screen.getByRole( 'combobox', { name: 'Font Size' } )
|
|
24
|
+
).toHaveAccessibleDescription( 'Currently selected: Small' );
|
|
25
|
+
} );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'should preserve the built-in description when validation is active', async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
render(
|
|
31
|
+
<form onSubmit={ ( e ) => e.preventDefault() }>
|
|
32
|
+
<ValidatedCustomSelectControl
|
|
33
|
+
label="Font Size"
|
|
34
|
+
options={ options }
|
|
35
|
+
value={ options[ 0 ] }
|
|
36
|
+
onChange={ () => {} }
|
|
37
|
+
required
|
|
38
|
+
/>
|
|
39
|
+
<button type="submit">Submit</button>
|
|
40
|
+
</form>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const combobox = await waitFor( () => {
|
|
44
|
+
return screen.getByRole( 'combobox', {
|
|
45
|
+
name: /^Font Size/,
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
await user.click( screen.getByRole( 'button', { name: 'Submit' } ) );
|
|
50
|
+
|
|
51
|
+
// The validation error targets the hidden delegate <select>, not
|
|
52
|
+
// the interactive combobox. The combobox's built-in description
|
|
53
|
+
// should be unaffected.
|
|
54
|
+
await waitFor( () => {
|
|
55
|
+
expect( combobox ).toHaveAccessibleDescription(
|
|
56
|
+
'Currently selected: Small'
|
|
57
|
+
);
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
} );
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ValidatedFormTokenField } from '../components';
|
|
4
|
+
|
|
5
|
+
describe( 'ValidatedFormTokenField', () => {
|
|
6
|
+
it( 'should preserve the built-in howto description', () => {
|
|
7
|
+
render(
|
|
8
|
+
<ValidatedFormTokenField
|
|
9
|
+
label="Tags"
|
|
10
|
+
value={ [] }
|
|
11
|
+
onChange={ () => {} }
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(
|
|
16
|
+
screen.getByRole( 'combobox', { name: 'Tags' } )
|
|
17
|
+
).toHaveAccessibleDescription(
|
|
18
|
+
expect.stringContaining( 'Separate with commas or the Enter key.' )
|
|
19
|
+
);
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
it( 'should preserve the built-in howto description when validation is active', async () => {
|
|
23
|
+
const user = userEvent.setup();
|
|
24
|
+
render(
|
|
25
|
+
<form>
|
|
26
|
+
<ValidatedFormTokenField
|
|
27
|
+
label="Tags"
|
|
28
|
+
value={ [] }
|
|
29
|
+
onChange={ () => {} }
|
|
30
|
+
required
|
|
31
|
+
/>
|
|
32
|
+
<button type="submit">Submit</button>
|
|
33
|
+
</form>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const input = screen.getByRole( 'combobox', { name: /^Tags/ } );
|
|
37
|
+
|
|
38
|
+
await user.click( screen.getByRole( 'button', { name: 'Submit' } ) );
|
|
39
|
+
|
|
40
|
+
// The validation error targets the hidden delegate input, not the
|
|
41
|
+
// interactive combobox. The combobox's built-in description should
|
|
42
|
+
// be unaffected.
|
|
43
|
+
await waitFor( () => {
|
|
44
|
+
expect(
|
|
45
|
+
screen.getByText( 'Constraints not satisfied' )
|
|
46
|
+
).toBeVisible();
|
|
47
|
+
} );
|
|
48
|
+
expect( input ).toHaveAccessibleDescription(
|
|
49
|
+
expect.stringContaining( 'Separate with commas or the Enter key.' )
|
|
50
|
+
);
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ValidatedInputControl } from '../components';
|
|
4
|
+
|
|
5
|
+
describe( 'ValidatedInputControl', () => {
|
|
6
|
+
it( 'should preserve the help description', () => {
|
|
7
|
+
render(
|
|
8
|
+
<ValidatedInputControl label="URL" help="Enter a full URL." />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(
|
|
12
|
+
screen.getByRole( 'textbox', { name: 'URL' } )
|
|
13
|
+
).toHaveAccessibleDescription( 'Enter a full URL.' );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
it( 'should append the validation error alongside the help description', async () => {
|
|
17
|
+
const user = userEvent.setup();
|
|
18
|
+
render(
|
|
19
|
+
<form>
|
|
20
|
+
<ValidatedInputControl
|
|
21
|
+
label="URL"
|
|
22
|
+
help="Enter a full URL."
|
|
23
|
+
required
|
|
24
|
+
/>
|
|
25
|
+
<button type="submit">Submit</button>
|
|
26
|
+
</form>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const input = screen.getByRole( 'textbox', { name: /^URL/ } );
|
|
30
|
+
|
|
31
|
+
await user.click( screen.getByRole( 'button', { name: 'Submit' } ) );
|
|
32
|
+
|
|
33
|
+
await waitFor( () => {
|
|
34
|
+
expect( input ).toHaveAccessibleDescription(
|
|
35
|
+
expect.stringContaining( 'Constraints not satisfied' )
|
|
36
|
+
);
|
|
37
|
+
} );
|
|
38
|
+
expect( input ).toHaveAccessibleDescription(
|
|
39
|
+
expect.stringContaining( 'Enter a full URL.' )
|
|
40
|
+
);
|
|
41
|
+
} );
|
|
42
|
+
} );
|