@@ -164,6 +164,9 @@ class Token<T> extends React.Component<TokenProps<T>, TokenState> {
164164
165165 return (
166166 < div
167+ role = "option"
168+ aria-selected = { this . props . selected }
169+ aria-label = { this . props . item ? this . props . item . toString ( ) : undefined }
167170 className = { `${ classes } ${ this . props . className } ` }
168171 onDragStart = { this . _onDragStart }
169172 onDragEnd = { this . _onDragEnd }
@@ -262,6 +265,7 @@ type TokenizingTextFieldState<T> = {
262265 focus : boolean ;
263266 completions : T [ ] ;
264267 selectedKeys : string [ ] ;
268+ activeDescendantId : string | null ;
265269} ;
266270
267271/*
@@ -432,6 +436,7 @@ export class TokenizingTextField<T> extends React.Component<
432436 inputValue : props . defaultValue || '' ,
433437 completions : [ ] ,
434438 selectedKeys : [ ] ,
439+ activeDescendantId : null ,
435440 } ;
436441 }
437442
@@ -445,7 +450,11 @@ export class TokenizingTextField<T> extends React.Component<
445450 }
446451
447452 componentDidUpdate ( prevProps : TokenizingTextFieldProps < T > ) {
448- if ( prevProps . tokens . length === 0 && this . props . tokens . length === 0 && this . state . inputValue . length === 0 ) {
453+ if (
454+ prevProps . tokens . length === 0 &&
455+ this . props . tokens . length === 0 &&
456+ this . state . inputValue . length === 0
457+ ) {
449458 if ( prevProps . defaultValue !== this . props . defaultValue ) {
450459 const newDefaultValue = this . props . defaultValue || '' ;
451460 this . setState ( { inputValue : newDefaultValue } ) ;
@@ -898,8 +907,22 @@ export class TokenizingTextField<T> extends React.Component<
898907
899908 // Rendering
900909
910+ _completionsId ( ) {
911+ return `${ this . _inputId } -completions` ;
912+ }
913+
914+ _valueDescriptionId ( ) {
915+ return `${ this . _inputId } -value` ;
916+ }
917+
918+ _onActiveDescendantChange = ( id : string | null ) => {
919+ this . setState ( { activeDescendantId : id } ) ;
920+ } ;
921+
901922 _inputComponent ( ) {
902- const props = {
923+ const hasCompletions = this . state . completions . length > 0 ;
924+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
925+ const props : any = {
903926 onCopy : this . _onCopy ,
904927 onCut : this . _onCut ,
905928 onPaste : this . _onPaste ,
@@ -911,6 +934,14 @@ export class TokenizingTextField<T> extends React.Component<
911934 tabIndex : this . props . tabIndex || 0 ,
912935 value : this . state . inputValue ,
913936 className : '' ,
937+ role : 'combobox' ,
938+ 'aria-expanded' : hasCompletions ,
939+ 'aria-haspopup' : 'listbox' ,
940+ 'aria-autocomplete' : 'list' ,
941+ 'aria-controls' : this . _completionsId ( ) ,
942+ 'aria-label' : this . props . label || undefined ,
943+ 'aria-describedby' : this . _valueDescriptionId ( ) ,
944+ 'aria-activedescendant' : this . state . activeDescendantId || undefined ,
914945 } ;
915946
916947 // If we can't accept additional tokens, override the events that would
@@ -973,6 +1004,9 @@ export class TokenizingTextField<T> extends React.Component<
9731004 'tokenizing-field-input' : true ,
9741005 'at-max-tokens' : this . _atMaxTokens ( ) ,
9751006 } ) ;
1007+ // Build a screen-reader-only description of current token values so that
1008+ // when the user focuses the input, the AT announces what is already in the field.
1009+ const tokenDescription = this . props . tokens . map ( t => t . toString ( ) ) . join ( ', ' ) ;
9761010 return (
9771011 < KeyCommandsRegion
9781012 key = "field-component"
@@ -989,10 +1023,18 @@ export class TokenizingTextField<T> extends React.Component<
9891023 { `${ this . props . label } :` }
9901024 </ label >
9911025 ) }
1026+ { /* Visually hidden span read by screen readers via aria-describedby on the input.
1027+ Describes the current token values so the field doesn't appear empty. */ }
1028+ < span
1029+ id = { this . _valueDescriptionId ( ) }
1030+ style = { { position : 'absolute' , left : - 9999 , width : 1 , height : 1 , overflow : 'hidden' } }
1031+ >
1032+ { tokenDescription }
1033+ </ span >
9921034 < div className = { fieldClasses } >
9931035 { this . state . inputValue . length > 0 ||
994- this . props . placeholder === undefined ||
995- this . props . tokens . length > 0 ? (
1036+ this . props . placeholder === undefined ||
1037+ this . props . tokens . length > 0 ? (
9961038 false
9971039 ) : (
9981040 < div className = "placeholder" > { this . props . placeholder } </ div >
@@ -1025,6 +1067,8 @@ export class TokenizingTextField<T> extends React.Component<
10251067 onFocus = { this . _onInputFocused }
10261068 onBlur = { this . _onInputBlurred }
10271069 onSelect = { this . _addToken }
1070+ onActiveDescendantChange = { this . _onActiveDescendantChange }
1071+ listboxId = { this . _completionsId ( ) }
10281072 />
10291073 ) ;
10301074 }
0 commit comments