@wordpress/components 28.4.0 → 28.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.
Files changed (250) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/build/autocomplete/autocompleter-ui.js +2 -0
  3. package/build/autocomplete/autocompleter-ui.js.map +1 -1
  4. package/build/base-control/styles/base-control-styles.js +8 -8
  5. package/build/base-control/styles/base-control-styles.js.map +1 -1
  6. package/build/border-control/styles.js +18 -24
  7. package/build/border-control/styles.js.map +1 -1
  8. package/build/color-palette/index.js +1 -1
  9. package/build/color-palette/index.js.map +1 -1
  10. package/build/custom-select-control/index.js +37 -14
  11. package/build/custom-select-control/index.js.map +1 -1
  12. package/build/custom-select-control/types.js.map +1 -1
  13. package/build/custom-select-control-v2/styles.js +9 -9
  14. package/build/custom-select-control-v2/styles.js.map +1 -1
  15. package/build/date-time/index.js +0 -7
  16. package/build/date-time/index.js.map +1 -1
  17. package/build/date-time/time/index.js +66 -38
  18. package/build/date-time/time/index.js.map +1 -1
  19. package/build/date-time/time/styles.js +11 -11
  20. package/build/date-time/time/styles.js.map +1 -1
  21. package/build/date-time/{time-input → time/time-input}/index.js +7 -7
  22. package/build/date-time/time/time-input/index.js.map +1 -0
  23. package/build/dropdown-menu-v2/styles.js +14 -14
  24. package/build/dropdown-menu-v2/styles.js.map +1 -1
  25. package/build/form-toggle/index.js +24 -24
  26. package/build/form-toggle/index.js.map +1 -1
  27. package/build/guide/index.js +2 -0
  28. package/build/guide/index.js.map +1 -1
  29. package/build/heading/types.js.map +1 -1
  30. package/build/modal/index.js +18 -11
  31. package/build/modal/index.js.map +1 -1
  32. package/build/query-controls/index.js +1 -1
  33. package/build/query-controls/index.js.map +1 -1
  34. package/build/radio-control/index.js +35 -8
  35. package/build/radio-control/index.js.map +1 -1
  36. package/build/radio-control/types.js.map +1 -1
  37. package/build/select-control/index.js +20 -8
  38. package/build/select-control/index.js.map +1 -1
  39. package/build/select-control/types.js.map +1 -1
  40. package/build/text-control/index.js +1 -0
  41. package/build/text-control/index.js.map +1 -1
  42. package/build/toggle-control/index.js +27 -25
  43. package/build/toggle-control/index.js.map +1 -1
  44. package/build/toggle-group-control/toggle-group-control/component.js +6 -1
  45. package/build/toggle-group-control/toggle-group-control/component.js.map +1 -1
  46. package/build/toggle-group-control/toggle-group-control-option/component.js +6 -1
  47. package/build/toggle-group-control/toggle-group-control-option/component.js.map +1 -1
  48. package/build/toggle-group-control/toggle-group-control-option-icon/component.js +14 -14
  49. package/build/toggle-group-control/toggle-group-control-option-icon/component.js.map +1 -1
  50. package/build/tooltip/index.js +12 -1
  51. package/build/tooltip/index.js.map +1 -1
  52. package/build/tree-select/index.js +1 -2
  53. package/build/tree-select/index.js.map +1 -1
  54. package/build/utils/config-values.js +6 -0
  55. package/build/utils/config-values.js.map +1 -1
  56. package/build-module/autocomplete/autocompleter-ui.js +2 -0
  57. package/build-module/autocomplete/autocompleter-ui.js.map +1 -1
  58. package/build-module/base-control/styles/base-control-styles.js +8 -8
  59. package/build-module/base-control/styles/base-control-styles.js.map +1 -1
  60. package/build-module/border-control/styles.js +13 -23
  61. package/build-module/border-control/styles.js.map +1 -1
  62. package/build-module/color-palette/index.js +1 -1
  63. package/build-module/color-palette/index.js.map +1 -1
  64. package/build-module/custom-select-control/index.js +38 -14
  65. package/build-module/custom-select-control/index.js.map +1 -1
  66. package/build-module/custom-select-control/types.js.map +1 -1
  67. package/build-module/custom-select-control-v2/styles.js +9 -9
  68. package/build-module/custom-select-control-v2/styles.js.map +1 -1
  69. package/build-module/date-time/index.js +1 -2
  70. package/build-module/date-time/index.js.map +1 -1
  71. package/build-module/date-time/time/index.js +66 -38
  72. package/build-module/date-time/time/index.js.map +1 -1
  73. package/build-module/date-time/time/styles.js +11 -11
  74. package/build-module/date-time/time/styles.js.map +1 -1
  75. package/build-module/date-time/{time-input → time/time-input}/index.js +7 -7
  76. package/build-module/date-time/time/time-input/index.js.map +1 -0
  77. package/build-module/dropdown-menu-v2/styles.js +14 -14
  78. package/build-module/dropdown-menu-v2/styles.js.map +1 -1
  79. package/build-module/form-toggle/index.js +23 -22
  80. package/build-module/form-toggle/index.js.map +1 -1
  81. package/build-module/guide/index.js +2 -0
  82. package/build-module/guide/index.js.map +1 -1
  83. package/build-module/heading/types.js.map +1 -1
  84. package/build-module/modal/index.js +17 -11
  85. package/build-module/modal/index.js.map +1 -1
  86. package/build-module/query-controls/index.js +1 -1
  87. package/build-module/query-controls/index.js.map +1 -1
  88. package/build-module/radio-control/index.js +36 -10
  89. package/build-module/radio-control/index.js.map +1 -1
  90. package/build-module/radio-control/types.js.map +1 -1
  91. package/build-module/select-control/index.js +20 -8
  92. package/build-module/select-control/index.js.map +1 -1
  93. package/build-module/select-control/types.js.map +1 -1
  94. package/build-module/text-control/index.js +1 -0
  95. package/build-module/text-control/index.js.map +1 -1
  96. package/build-module/toggle-control/index.js +26 -24
  97. package/build-module/toggle-control/index.js.map +1 -1
  98. package/build-module/toggle-group-control/toggle-group-control/component.js +6 -1
  99. package/build-module/toggle-group-control/toggle-group-control/component.js.map +1 -1
  100. package/build-module/toggle-group-control/toggle-group-control-option/component.js +6 -1
  101. package/build-module/toggle-group-control/toggle-group-control-option/component.js.map +1 -1
  102. package/build-module/toggle-group-control/toggle-group-control-option-icon/component.js +14 -14
  103. package/build-module/toggle-group-control/toggle-group-control-option-icon/component.js.map +1 -1
  104. package/build-module/tooltip/index.js +13 -2
  105. package/build-module/tooltip/index.js.map +1 -1
  106. package/build-module/tree-select/index.js +1 -2
  107. package/build-module/tree-select/index.js.map +1 -1
  108. package/build-module/utils/config-values.js +6 -0
  109. package/build-module/utils/config-values.js.map +1 -1
  110. package/build-style/style-rtl.css +60 -24
  111. package/build-style/style.css +60 -24
  112. package/build-types/autocomplete/autocompleter-ui.d.ts.map +1 -1
  113. package/build-types/border-control/styles.d.ts.map +1 -1
  114. package/build-types/button/stories/e2e/index.story.d.ts.map +1 -1
  115. package/build-types/color-palette/index.d.ts.map +1 -1
  116. package/build-types/color-palette/styles.d.ts +2 -2
  117. package/build-types/color-picker/styles.d.ts +3 -1
  118. package/build-types/color-picker/styles.d.ts.map +1 -1
  119. package/build-types/custom-select-control/index.d.ts +2 -2
  120. package/build-types/custom-select-control/index.d.ts.map +1 -1
  121. package/build-types/custom-select-control/stories/index.story.d.ts +3 -3
  122. package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
  123. package/build-types/custom-select-control/types.d.ts +7 -7
  124. package/build-types/custom-select-control/types.d.ts.map +1 -1
  125. package/build-types/custom-select-control-v2/styles.d.ts +16 -28
  126. package/build-types/custom-select-control-v2/styles.d.ts.map +1 -1
  127. package/build-types/date-time/date/styles.d.ts +2 -2
  128. package/build-types/date-time/index.d.ts +1 -2
  129. package/build-types/date-time/index.d.ts.map +1 -1
  130. package/build-types/date-time/stories/time.story.d.ts +5 -0
  131. package/build-types/date-time/stories/time.story.d.ts.map +1 -1
  132. package/build-types/date-time/time/index.d.ts +3 -0
  133. package/build-types/date-time/time/index.d.ts.map +1 -1
  134. package/build-types/date-time/time/styles.d.ts.map +1 -1
  135. package/build-types/date-time/{time-input → time/time-input}/index.d.ts +1 -1
  136. package/build-types/date-time/time/time-input/index.d.ts.map +1 -0
  137. package/build-types/date-time/time/time-input/test/index.d.ts.map +1 -0
  138. package/build-types/dropdown-menu-v2/styles.d.ts +24 -42
  139. package/build-types/dropdown-menu-v2/styles.d.ts.map +1 -1
  140. package/build-types/form-toggle/index.d.ts +2 -5
  141. package/build-types/form-toggle/index.d.ts.map +1 -1
  142. package/build-types/guide/index.d.ts.map +1 -1
  143. package/build-types/heading/component.d.ts +1 -1
  144. package/build-types/heading/types.d.ts +1 -1
  145. package/build-types/heading/types.d.ts.map +1 -1
  146. package/build-types/modal/index.d.ts.map +1 -1
  147. package/build-types/navigation/styles/navigation-styles.d.ts +2 -2
  148. package/build-types/palette-edit/styles.d.ts +2 -2
  149. package/build-types/query-controls/index.d.ts.map +1 -1
  150. package/build-types/radio-control/index.d.ts.map +1 -1
  151. package/build-types/radio-control/stories/index.story.d.ts +1 -0
  152. package/build-types/radio-control/stories/index.story.d.ts.map +1 -1
  153. package/build-types/radio-control/test/index.d.ts +2 -0
  154. package/build-types/radio-control/test/index.d.ts.map +1 -0
  155. package/build-types/radio-control/types.d.ts +4 -0
  156. package/build-types/radio-control/types.d.ts.map +1 -1
  157. package/build-types/select-control/index.d.ts +4 -1
  158. package/build-types/select-control/index.d.ts.map +1 -1
  159. package/build-types/select-control/stories/index.story.d.ts +9 -3
  160. package/build-types/select-control/stories/index.story.d.ts.map +1 -1
  161. package/build-types/select-control/types.d.ts +27 -27
  162. package/build-types/select-control/types.d.ts.map +1 -1
  163. package/build-types/tabs/styles.d.ts +8 -14
  164. package/build-types/tabs/styles.d.ts.map +1 -1
  165. package/build-types/text-control/index.d.ts +1 -0
  166. package/build-types/text-control/index.d.ts.map +1 -1
  167. package/build-types/toggle-control/index.d.ts +3 -9
  168. package/build-types/toggle-control/index.d.ts.map +1 -1
  169. package/build-types/toggle-group-control/toggle-group-control/component.d.ts +6 -1
  170. package/build-types/toggle-group-control/toggle-group-control/component.d.ts.map +1 -1
  171. package/build-types/toggle-group-control/toggle-group-control-option/component.d.ts +6 -1
  172. package/build-types/toggle-group-control/toggle-group-control-option/component.d.ts.map +1 -1
  173. package/build-types/toggle-group-control/toggle-group-control-option-icon/component.d.ts +14 -14
  174. package/build-types/tooltip/index.d.ts.map +1 -1
  175. package/build-types/tooltip/test/utils/index.d.ts +1 -5
  176. package/build-types/tooltip/test/utils/index.d.ts.map +1 -1
  177. package/build-types/tree-select/index.d.ts +1 -1
  178. package/build-types/tree-select/index.d.ts.map +1 -1
  179. package/build-types/utils/config-values.d.ts +6 -0
  180. package/package.json +20 -20
  181. package/src/alignment-matrix-control/test/index.tsx +1 -3
  182. package/src/autocomplete/autocompleter-ui.tsx +2 -0
  183. package/src/autocomplete/style.scss +0 -6
  184. package/src/base-control/styles/base-control-styles.ts +1 -1
  185. package/src/border-control/styles.ts +0 -5
  186. package/src/button/stories/e2e/index.story.tsx +103 -21
  187. package/src/button/style.scss +49 -21
  188. package/src/button/test/index.tsx +18 -40
  189. package/src/circular-option-picker/test/index.tsx +1 -4
  190. package/src/color-palette/index.tsx +22 -20
  191. package/src/composite/legacy/test/index.tsx +2 -18
  192. package/src/custom-select-control/index.tsx +55 -25
  193. package/src/custom-select-control/test/index.tsx +47 -30
  194. package/src/custom-select-control/types.ts +7 -7
  195. package/src/custom-select-control-v2/styles.ts +7 -6
  196. package/src/custom-select-control-v2/test/index.tsx +15 -19
  197. package/src/date-time/index.ts +1 -2
  198. package/src/date-time/stories/time.story.tsx +17 -0
  199. package/src/date-time/time/index.tsx +46 -16
  200. package/src/date-time/time/styles.ts +1 -0
  201. package/src/date-time/{time-input → time/time-input}/index.tsx +9 -9
  202. package/src/dropdown-menu-v2/styles.ts +18 -17
  203. package/src/dropdown-menu-v2/test/index.tsx +1 -4
  204. package/src/font-size-picker/test/index.tsx +50 -43
  205. package/src/form-toggle/index.tsx +23 -21
  206. package/src/guide/index.tsx +2 -0
  207. package/src/heading/types.ts +1 -4
  208. package/src/modal/index.tsx +21 -20
  209. package/src/placeholder/style.scss +11 -2
  210. package/src/query-controls/index.tsx +5 -1
  211. package/src/radio-control/index.tsx +48 -7
  212. package/src/radio-control/stories/index.story.tsx +23 -0
  213. package/src/radio-control/style.scss +26 -2
  214. package/src/radio-control/test/index.tsx +274 -0
  215. package/src/radio-control/types.ts +4 -0
  216. package/src/select-control/README.md +8 -1
  217. package/src/select-control/index.tsx +33 -22
  218. package/src/select-control/test/select-control.tsx +148 -4
  219. package/src/select-control/types.ts +70 -65
  220. package/src/tab-panel/test/index.tsx +1 -8
  221. package/src/tabs/test/index.tsx +68 -84
  222. package/src/text-control/README.md +1 -0
  223. package/src/text-control/index.tsx +1 -0
  224. package/src/text-control/style.scss +5 -0
  225. package/src/toggle-control/README.md +9 -0
  226. package/src/toggle-control/index.tsx +25 -22
  227. package/src/toggle-control/style.scss +2 -1
  228. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +6 -6
  229. package/src/toggle-group-control/test/index.tsx +0 -6
  230. package/src/toggle-group-control/toggle-group-control/README.md +13 -1
  231. package/src/toggle-group-control/toggle-group-control/component.tsx +6 -1
  232. package/src/toggle-group-control/toggle-group-control-option/README.md +6 -1
  233. package/src/toggle-group-control/toggle-group-control-option/component.tsx +6 -1
  234. package/src/toggle-group-control/toggle-group-control-option-icon/README.md +1 -1
  235. package/src/toggle-group-control/toggle-group-control-option-icon/component.tsx +14 -14
  236. package/src/tooltip/index.tsx +15 -2
  237. package/src/tooltip/test/index.tsx +0 -5
  238. package/src/tooltip/test/utils/index.tsx +5 -5
  239. package/src/tree-select/index.tsx +1 -2
  240. package/src/utils/config-values.js +6 -0
  241. package/tsconfig.tsbuildinfo +1 -1
  242. package/build/date-time/time-input/index.js.map +0 -1
  243. package/build-module/date-time/time-input/index.js.map +0 -1
  244. package/build-types/date-time/stories/time-input.story.d.ts +0 -12
  245. package/build-types/date-time/stories/time-input.story.d.ts.map +0 -1
  246. package/build-types/date-time/time-input/index.d.ts.map +0 -1
  247. package/build-types/date-time/time-input/test/index.d.ts.map +0 -1
  248. package/src/date-time/stories/time-input.story.tsx +0 -36
  249. /package/build-types/date-time/{time-input → time/time-input}/test/index.d.ts +0 -0
  250. /package/src/date-time/{time-input → time/time-input}/test/index.tsx +0 -0
@@ -183,8 +183,17 @@
183
183
  }
184
184
  }
185
185
 
186
- // By painting the borders here, we enable them to be replaced by the Border control.
187
- @include placeholder-style();
186
+ &::before {
187
+ content: "";
188
+ position: absolute;
189
+ top: 0;
190
+ right: 0;
191
+ bottom: 0;
192
+ left: 0;
193
+ pointer-events: none;
194
+ background: currentColor;
195
+ opacity: 0.1;
196
+ }
188
197
 
189
198
  overflow: hidden;
190
199
  .is-selected & {
@@ -85,7 +85,11 @@ export function QueryControls( {
85
85
  __next40pxDefaultSize={ __next40pxDefaultSize }
86
86
  key="query-controls-order-select"
87
87
  label={ __( 'Order by' ) }
88
- value={ `${ orderBy }/${ order }` }
88
+ value={
89
+ orderBy === undefined || order === undefined
90
+ ? undefined
91
+ : `${ orderBy }/${ order }`
92
+ }
89
93
  options={ [
90
94
  {
91
95
  label: __( 'Newest to oldest' ),
@@ -16,6 +16,16 @@ import BaseControl from '../base-control';
16
16
  import type { WordPressComponentProps } from '../context';
17
17
  import type { RadioControlProps } from './types';
18
18
  import { VStack } from '../v-stack';
19
+ import { useBaseControlProps } from '../base-control/hooks';
20
+ import { StyledHelp } from '../base-control/styles/base-control-styles';
21
+
22
+ function generateOptionDescriptionId( radioGroupId: string, index: number ) {
23
+ return `${ radioGroupId }-${ index }-option-description`;
24
+ }
25
+
26
+ function generateOptionId( radioGroupId: string, index: number ) {
27
+ return `${ radioGroupId }-${ index }`;
28
+ }
19
29
 
20
30
  /**
21
31
  * Render a user interface to select the user type using radio inputs.
@@ -53,13 +63,23 @@ export function RadioControl(
53
63
  onChange,
54
64
  hideLabelFromVision,
55
65
  options = [],
66
+ id: preferredId,
56
67
  ...additionalProps
57
68
  } = props;
58
- const instanceId = useInstanceId( RadioControl );
59
- const id = `inspector-radio-control-${ instanceId }`;
69
+ const id = useInstanceId(
70
+ RadioControl,
71
+ 'inspector-radio-control',
72
+ preferredId
73
+ );
74
+
60
75
  const onChangeValue = ( event: ChangeEvent< HTMLInputElement > ) =>
61
76
  onChange( event.target.value );
62
77
 
78
+ // Use `useBaseControlProps` to get the id of the help text.
79
+ const {
80
+ controlProps: { 'aria-describedby': helpTextId },
81
+ } = useBaseControlProps( { id, help } );
82
+
63
83
  if ( ! options?.length ) {
64
84
  return null;
65
85
  }
@@ -73,14 +93,19 @@ export function RadioControl(
73
93
  help={ help }
74
94
  className={ clsx( className, 'components-radio-control' ) }
75
95
  >
76
- <VStack spacing={ 2 }>
96
+ <VStack
97
+ spacing={ 3 }
98
+ className={ clsx( 'components-radio-control__group-wrapper', {
99
+ 'has-help': !! help,
100
+ } ) }
101
+ >
77
102
  { options.map( ( option, index ) => (
78
103
  <div
79
- key={ `${ id }-${ index }` }
104
+ key={ generateOptionId( id, index ) }
80
105
  className="components-radio-control__option"
81
106
  >
82
107
  <input
83
- id={ `${ id }-${ index }` }
108
+ id={ generateOptionId( id, index ) }
84
109
  className="components-radio-control__input"
85
110
  type="radio"
86
111
  name={ id }
@@ -88,16 +113,32 @@ export function RadioControl(
88
113
  onChange={ onChangeValue }
89
114
  checked={ option.value === selected }
90
115
  aria-describedby={
91
- !! help ? `${ id }__help` : undefined
116
+ clsx( [
117
+ !! option.description &&
118
+ generateOptionDescriptionId(
119
+ id,
120
+ index
121
+ ),
122
+ helpTextId,
123
+ ] ) || undefined
92
124
  }
93
125
  { ...additionalProps }
94
126
  />
95
127
  <label
96
128
  className="components-radio-control__label"
97
- htmlFor={ `${ id }-${ index }` }
129
+ htmlFor={ generateOptionId( id, index ) }
98
130
  >
99
131
  { option.label }
100
132
  </label>
133
+ { !! option.description ? (
134
+ <StyledHelp
135
+ __nextHasNoMarginBottom
136
+ id={ generateOptionDescriptionId( id, index ) }
137
+ className="components-radio-control__option-description"
138
+ >
139
+ { option.description }
140
+ </StyledHelp>
141
+ ) : null }
101
142
  </div>
102
143
  ) ) }
103
144
  </VStack>
@@ -68,3 +68,26 @@ Default.args = {
68
68
  { label: 'Password Protected', value: 'password' },
69
69
  ],
70
70
  };
71
+
72
+ export const WithOptionDescriptions: StoryFn< typeof RadioControl > =
73
+ Template.bind( {} );
74
+ WithOptionDescriptions.args = {
75
+ ...Default.args,
76
+ options: [
77
+ {
78
+ label: 'Public',
79
+ value: 'public',
80
+ description: 'Visible to everyone',
81
+ },
82
+ {
83
+ label: 'Private',
84
+ value: 'private',
85
+ description: 'Only visible to you',
86
+ },
87
+ {
88
+ label: 'Password Protected',
89
+ value: 'password',
90
+ description: 'Protected by a password',
91
+ },
92
+ ],
93
+ };
@@ -1,13 +1,23 @@
1
+ .components-radio-control__group-wrapper.has-help {
2
+ margin-block-end: $grid-unit-15;
3
+ }
4
+
1
5
  .components-radio-control__option {
2
- display: flex;
6
+ display: grid;
7
+ grid-template-columns: auto 1fr;
8
+ grid-template-rows: auto minmax(0, max-content);
9
+ column-gap: $grid-unit-10;
3
10
  align-items: center;
4
11
  }
5
12
 
6
13
  .components-radio-control__input[type="radio"] {
14
+ grid-column: 1;
15
+ grid-row: 1;
16
+
7
17
  @include radio-control;
8
18
 
9
19
  display: inline-flex;
10
- margin: 0 $grid-unit-10 0 0;
20
+ margin: 0;
11
21
  padding: 0;
12
22
  appearance: none;
13
23
  cursor: pointer;
@@ -28,6 +38,9 @@
28
38
  }
29
39
 
30
40
  .components-radio-control__label {
41
+ grid-column: 2;
42
+ grid-row: 1;
43
+
31
44
  cursor: pointer;
32
45
  line-height: $radio-input-size-sm;
33
46
 
@@ -35,3 +48,14 @@
35
48
  line-height: $radio-input-size;
36
49
  }
37
50
  }
51
+
52
+ .components-radio-control__option-description {
53
+ grid-column: 2;
54
+ grid-row: 2;
55
+
56
+ padding-block-start: $grid-unit-05;
57
+
58
+ // Override the top margin of the StyledHelp component from BaseControl.
59
+ // TODO: we should tweak the StyledHelp component to not have a top margin.
60
+ margin-top: 0;
61
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useState } from '@wordpress/element';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import RadioControl from '../';
16
+
17
+ const ControlledRadioControl = ( {
18
+ ...props
19
+ }: React.ComponentProps< typeof RadioControl > ) => {
20
+ const [ option, setOption ] = useState( props.selected );
21
+
22
+ return (
23
+ <RadioControl
24
+ { ...props }
25
+ onChange={ ( newValue ) => {
26
+ setOption( newValue );
27
+ props.onChange?.( newValue );
28
+ } }
29
+ selected={ option }
30
+ />
31
+ );
32
+ };
33
+
34
+ const defaultProps = {
35
+ options: [
36
+ { label: 'Mouse', value: 'mouse' },
37
+ { label: 'Cat', value: 'cat' },
38
+ { label: 'Dog', value: 'dog' },
39
+ ],
40
+ label: 'Animal',
41
+ };
42
+
43
+ const defaultPropsWithDescriptions = {
44
+ ...defaultProps,
45
+ options: defaultProps.options.map( ( option, index ) => ( {
46
+ ...option,
47
+ description: `This is the option number ${ index + 1 }.`,
48
+ } ) ),
49
+ };
50
+
51
+ describe.each( [
52
+ // TODO: `RadioControl` doesn't currently support uncontrolled mode.
53
+ // [ 'uncontrolled', RadioControl ],
54
+ [ 'controlled', ControlledRadioControl ],
55
+ ] )( 'RadioControl %s', ( ...modeAndComponent ) => {
56
+ const [ , Component ] = modeAndComponent;
57
+
58
+ describe( 'semantics and labelling', () => {
59
+ it( 'should render radio inputs with accessible labels', () => {
60
+ const onChangeSpy = jest.fn();
61
+ render(
62
+ <Component { ...defaultProps } onChange={ onChangeSpy } />
63
+ );
64
+
65
+ for ( const option of defaultProps.options ) {
66
+ const optionEl = screen.getByRole( 'radio', {
67
+ name: option.label,
68
+ } );
69
+ expect( optionEl ).toBeVisible();
70
+ expect( optionEl ).not.toBeChecked();
71
+ }
72
+ } );
73
+
74
+ it( 'should not select have a selected value when the `selected` prop does not match any available options', () => {
75
+ const onChangeSpy = jest.fn();
76
+ render(
77
+ <Component { ...defaultProps } onChange={ onChangeSpy } />
78
+ );
79
+
80
+ expect(
81
+ screen.queryByRole( 'radio', {
82
+ checked: true,
83
+ } )
84
+ ).not.toBeInTheDocument();
85
+ } );
86
+
87
+ it( 'should render mutually exclusive radio inputs', () => {
88
+ const onChangeSpy = jest.fn();
89
+ render(
90
+ <Component
91
+ { ...defaultProps }
92
+ onChange={ onChangeSpy }
93
+ selected={ defaultProps.options[ 1 ].value }
94
+ />
95
+ );
96
+
97
+ expect(
98
+ screen.getByRole( 'radio', {
99
+ checked: true,
100
+ } )
101
+ ).toHaveAccessibleName( defaultProps.options[ 1 ].label );
102
+ } );
103
+
104
+ it( 'should use the group help text to describe individual options', () => {
105
+ const onChangeSpy = jest.fn();
106
+ render(
107
+ <Component
108
+ { ...defaultProps }
109
+ onChange={ onChangeSpy }
110
+ selected={ defaultProps.options[ 1 ].value }
111
+ help="Select your favorite animal."
112
+ />
113
+ );
114
+
115
+ for ( const option of defaultProps.options ) {
116
+ expect(
117
+ screen.getByRole( 'radio', { name: option.label } )
118
+ ).toHaveAccessibleDescription( 'Select your favorite animal.' );
119
+ }
120
+ } );
121
+
122
+ it( 'should use the option description text to describe individual options', () => {
123
+ const onChangeSpy = jest.fn();
124
+ render(
125
+ <Component
126
+ { ...defaultPropsWithDescriptions }
127
+ onChange={ onChangeSpy }
128
+ selected={ defaultProps.options[ 1 ].value }
129
+ />
130
+ );
131
+
132
+ let index = 1;
133
+ for ( const option of defaultProps.options ) {
134
+ expect(
135
+ screen.getByRole( 'radio', { name: option.label } )
136
+ ).toHaveAccessibleDescription(
137
+ `This is the option number ${ index }.`
138
+ );
139
+ index += 1;
140
+ }
141
+ } );
142
+
143
+ it( 'should use both the option description text and the group help text to describe individual options', () => {
144
+ const onChangeSpy = jest.fn();
145
+ render(
146
+ <Component
147
+ { ...defaultPropsWithDescriptions }
148
+ onChange={ onChangeSpy }
149
+ selected={ defaultProps.options[ 1 ].value }
150
+ help="Select your favorite animal"
151
+ />
152
+ );
153
+
154
+ let index = 1;
155
+ for ( const option of defaultProps.options ) {
156
+ expect(
157
+ screen.getByRole( 'radio', { name: option.label } )
158
+ ).toHaveAccessibleDescription(
159
+ `This is the option number ${ index }. Select your favorite animal`
160
+ );
161
+ index += 1;
162
+ }
163
+ } );
164
+ } );
165
+
166
+ describe( 'interaction', () => {
167
+ it( 'should select a new value when clicking on the radio input', async () => {
168
+ const user = userEvent.setup();
169
+ const onChangeSpy = jest.fn();
170
+ render(
171
+ <Component { ...defaultProps } onChange={ onChangeSpy } />
172
+ );
173
+
174
+ // Click on the third radio, make sure it's selected.
175
+ await user.click(
176
+ screen.getByRole( 'radio', {
177
+ name: defaultProps.options[ 2 ].label,
178
+ } )
179
+ );
180
+ expect(
181
+ screen.getByRole( 'radio', {
182
+ checked: true,
183
+ } )
184
+ ).toHaveAccessibleName( defaultProps.options[ 2 ].label );
185
+
186
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
187
+ expect( onChangeSpy ).toHaveBeenLastCalledWith(
188
+ defaultProps.options[ 2 ].value
189
+ );
190
+ } );
191
+
192
+ it( 'should select a new value when clicking on the radio label', async () => {
193
+ const user = userEvent.setup();
194
+ const onChangeSpy = jest.fn();
195
+ render(
196
+ <Component { ...defaultProps } onChange={ onChangeSpy } />
197
+ );
198
+
199
+ // Click on the second radio's label, make sure it selects the associated radio.
200
+ await user.click(
201
+ screen.getByText( defaultProps.options[ 1 ].label )
202
+ );
203
+ expect(
204
+ screen.getByRole( 'radio', {
205
+ checked: true,
206
+ } )
207
+ ).toHaveAccessibleName( defaultProps.options[ 1 ].label );
208
+
209
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
210
+ expect( onChangeSpy ).toHaveBeenLastCalledWith(
211
+ defaultProps.options[ 1 ].value
212
+ );
213
+ } );
214
+
215
+ it( 'should select a new value when using the arrow keys', async () => {
216
+ const user = userEvent.setup();
217
+ const onChangeSpy = jest.fn();
218
+ render(
219
+ <Component { ...defaultProps } onChange={ onChangeSpy } />
220
+ );
221
+
222
+ await user.tab();
223
+
224
+ expect(
225
+ screen.getByRole( 'radio', {
226
+ name: defaultProps.options[ 0 ].label,
227
+ } )
228
+ ).toHaveFocus();
229
+
230
+ await user.keyboard( '{ArrowDown}' );
231
+
232
+ expect(
233
+ screen.getByRole( 'radio', {
234
+ checked: true,
235
+ name: defaultProps.options[ 1 ].label,
236
+ } )
237
+ ).toHaveFocus();
238
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
239
+ expect( onChangeSpy ).toHaveBeenLastCalledWith(
240
+ defaultProps.options[ 1 ].value
241
+ );
242
+
243
+ await user.keyboard( '{ArrowDown}' );
244
+ await user.keyboard( '{ArrowDown}' );
245
+
246
+ // The selection wrap around.
247
+ expect(
248
+ screen.getByRole( 'radio', {
249
+ checked: true,
250
+ name: defaultProps.options[ 0 ].label,
251
+ } )
252
+ ).toHaveFocus();
253
+ // TODO: why called twice for every interaction?
254
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 6 );
255
+ expect( onChangeSpy ).toHaveBeenLastCalledWith(
256
+ defaultProps.options[ 0 ].value
257
+ );
258
+
259
+ await user.keyboard( '{ArrowUp}' );
260
+
261
+ expect(
262
+ screen.getByRole( 'radio', {
263
+ checked: true,
264
+ name: defaultProps.options[ 2 ].label,
265
+ } )
266
+ ).toHaveFocus();
267
+
268
+ expect( onChangeSpy ).toHaveBeenCalledTimes( 8 );
269
+ expect( onChangeSpy ).toHaveBeenLastCalledWith(
270
+ defaultProps.options[ 2 ].value
271
+ );
272
+ } );
273
+ } );
274
+ } );
@@ -24,6 +24,10 @@ export type RadioControlProps = Pick<
24
24
  * The internal value compared against select and passed to onChange
25
25
  */
26
26
  value: string;
27
+ /**
28
+ * Optional help text to be shown in addition the label.
29
+ */
30
+ description?: string;
27
31
  }[];
28
32
  /**
29
33
  * The value property of the currently selected option.
@@ -190,7 +190,7 @@ In most cases, it is preferable to use the `FormTokenField` or `CheckboxControl`
190
190
 
191
191
  #### options
192
192
 
193
- An array of objects containing the following properties:
193
+ An array of objects containing the following properties, as well as any other `option` element attributes:
194
194
 
195
195
  - `label`: (string) The label to be shown to the user.
196
196
  - `value`: (string) The internal value used to choose the selected value. This is also the value passed to onChange when the option is selected.
@@ -214,6 +214,13 @@ If multiple is false the value received is a single value with the new selected
214
214
  - Type: `function`
215
215
  - Required: Yes
216
216
 
217
+ #### value
218
+
219
+ The value of the selected option. If `multiple` is true, the `value` should be an array with the values of the selected options.
220
+
221
+ - Type: `String|String[]`
222
+ - Required: No
223
+
217
224
  #### variant
218
225
 
219
226
  The style variant of the control.
@@ -26,8 +26,24 @@ function useUniqueId( idProp?: string ) {
26
26
  return idProp || id;
27
27
  }
28
28
 
29
- function UnforwardedSelectControl(
30
- props: WordPressComponentProps< SelectControlProps, 'select', false >,
29
+ function SelectOptions( {
30
+ options,
31
+ }: {
32
+ options: NonNullable< SelectControlProps[ 'options' ] >;
33
+ } ) {
34
+ return options.map( ( { id, label, value, ...optionProps }, index ) => {
35
+ const key = id || `${ label }-${ value }-${ index }`;
36
+
37
+ return (
38
+ <option key={ key } value={ value } { ...optionProps }>
39
+ { label }
40
+ </option>
41
+ );
42
+ } );
43
+ }
44
+
45
+ function UnforwardedSelectControl< V extends string >(
46
+ props: WordPressComponentProps< SelectControlProps< V >, 'select', false >,
31
47
  ref: React.ForwardedRef< HTMLSelectElement >
32
48
  ) {
33
49
  const {
@@ -66,12 +82,14 @@ function UnforwardedSelectControl(
66
82
  const selectedOptions = Array.from( event.target.options ).filter(
67
83
  ( { selected } ) => selected
68
84
  );
69
- const newValues = selectedOptions.map( ( { value } ) => value );
85
+ const newValues = selectedOptions.map(
86
+ ( { value } ) => value as V
87
+ );
70
88
  props.onChange?.( newValues, { event } );
71
89
  return;
72
90
  }
73
91
 
74
- props.onChange?.( event.target.value, { event } );
92
+ props.onChange?.( event.target.value as V, { event } );
75
93
  };
76
94
 
77
95
  const classes = clsx( 'components-select-control', className );
@@ -115,23 +133,7 @@ function UnforwardedSelectControl(
115
133
  value={ valueProp }
116
134
  variant={ variant }
117
135
  >
118
- { children ||
119
- options.map( ( option, index ) => {
120
- const key =
121
- option.id ||
122
- `${ option.label }-${ option.value }-${ index }`;
123
-
124
- return (
125
- <option
126
- key={ key }
127
- value={ option.value }
128
- disabled={ option.disabled }
129
- hidden={ option.hidden }
130
- >
131
- { option.label }
132
- </option>
133
- );
134
- } ) }
136
+ { children || <SelectOptions options={ options } /> }
135
137
  </Select>
136
138
  </StyledInputBase>
137
139
  </BaseControl>
@@ -151,6 +153,7 @@ function UnforwardedSelectControl(
151
153
  *
152
154
  * return (
153
155
  * <SelectControl
156
+ * __nextHasNoMarginBottom
154
157
  * label="Size"
155
158
  * value={ size }
156
159
  * options={ [
@@ -164,6 +167,14 @@ function UnforwardedSelectControl(
164
167
  * };
165
168
  * ```
166
169
  */
167
- export const SelectControl = forwardRef( UnforwardedSelectControl );
170
+ export const SelectControl = forwardRef( UnforwardedSelectControl ) as <
171
+ V extends string,
172
+ >(
173
+ props: WordPressComponentProps<
174
+ SelectControlProps< V >,
175
+ 'select',
176
+ false
177
+ > & { ref?: React.Ref< HTMLSelectElement > }
178
+ ) => React.JSX.Element | null;
168
179
 
169
180
  export default SelectControl;