@@ -84,8 +84,12 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
8484 }
8585
8686 this . _defaultId = props . deterministicId ! ( )
87+ this . _messagesId = props . deterministicId ! ( 'Checkbox-messages' )
88+ this . _labelId = props . deterministicId ! ( 'Checkbox-label' )
8789 }
8890 private readonly _defaultId : string
91+ private readonly _messagesId : string
92+ private readonly _labelId : string
8993 private _input : HTMLInputElement | null = null
9094
9195 ref : Element | null = null
@@ -186,6 +190,10 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
186190 )
187191 }
188192
193+ get hasMessages ( ) {
194+ return ! ! this . props . messages && this . props . messages . length > 0
195+ }
196+
189197 get invalid ( ) {
190198 return ! ! this . props . messages ?. find (
191199 ( m ) => m . type === 'newError' || m . type === 'error'
@@ -217,6 +225,24 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
217225 `[Checkbox] The \`simple\` variant does not support the \`labelPlacement\` property. Use the \`toggle\` variant instead.`
218226 )
219227
228+ // The label text gets its own id so that, when there are messages, the
229+ // input's accessible name can point at the label only (via
230+ // `aria-labelledby`) instead of also including the message text.
231+ const labelContent = (
232+ < span id = { this . _labelId } >
233+ { label }
234+ { isRequired && label && (
235+ < span
236+ css = { this . invalid ? styles ?. requiredInvalid : { } }
237+ aria-hidden = { true }
238+ >
239+ { ' ' }
240+ *
241+ </ span >
242+ ) }
243+ </ span >
244+ )
245+
220246 if ( variant === 'toggle' ) {
221247 return (
222248 < ToggleFacade
@@ -232,16 +258,7 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
232258 themeOverride = { themeOverride }
233259 invalid = { this . invalid }
234260 >
235- { label }
236- { isRequired && label && (
237- < span
238- css = { this . invalid ? styles ?. requiredInvalid : { } }
239- aria-hidden = { true }
240- >
241- { ' ' }
242- *
243- </ span >
244- ) }
261+ { labelContent }
245262 </ ToggleFacade >
246263 )
247264 } else {
@@ -257,16 +274,7 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
257274 themeOverride = { themeOverride }
258275 invalid = { this . invalid }
259276 >
260- { label }
261- { isRequired && label && (
262- < span
263- css = { this . invalid ? styles ?. requiredInvalid : { } }
264- aria-hidden = { true }
265- >
266- { ' ' }
267- *
268- </ span >
269- ) }
277+ { labelContent }
270278 </ CheckboxFacade >
271279 )
272280 }
@@ -286,7 +294,7 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
286294 : styles ?. indentedError )
287295 }
288296 >
289- < FormFieldMessages messages = { messages } />
297+ < FormFieldMessages id = { this . _messagesId } messages = { messages } />
290298 </ View >
291299 ) : null
292300 }
@@ -338,6 +346,24 @@ class Checkbox extends Component<CheckboxProps, CheckboxState> {
338346 aria-readonly = { readOnly ? true : undefined }
339347 aria-checked = { indeterminate ? 'mixed' : undefined }
340348 aria-invalid = { this . invalid ? 'true' : undefined }
349+ // Keep messages in the description so the accessible name contains only the label.
350+ aria-labelledby = {
351+ this . hasMessages
352+ ? ( ( props as Record < string , unknown > ) [
353+ 'aria-labelledby'
354+ ] as string ) || this . _labelId
355+ : ( ( props as Record < string , unknown > ) [ 'aria-labelledby' ] as
356+ | string
357+ | undefined )
358+ }
359+ aria-describedby = {
360+ [
361+ ( props as Record < string , unknown > ) [ 'aria-describedby' ] ,
362+ this . hasMessages ? this . _messagesId : null
363+ ]
364+ . filter ( Boolean )
365+ . join ( ' ' ) || undefined
366+ }
341367 css = { styles ?. input }
342368 onClickCapture = { ( e ) => {
343369 if ( readOnly ) {
0 commit comments