Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit fa27ba6

Browse files
bbrittocopybara-github
authored andcommitted
feat(select): Select foundation preserves describedby elements on validate
PiperOrigin-RevId: 512630101
1 parent 7ab3246 commit fa27ba6

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

packages/mdc-select/foundation.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export class MDCSelectFoundation extends MDCFoundation<MDCSelectAdapter> {
9393

9494
private readonly leadingIcon: MDCSelectIconFoundation|undefined;
9595
private readonly helperText: MDCSelectHelperTextFoundation|undefined;
96+
private readonly ariaDescribedbyIds: string[];
9697

9798
// Disabled state
9899
private disabled = false;
@@ -121,6 +122,11 @@ export class MDCSelectFoundation extends MDCFoundation<MDCSelectAdapter> {
121122

122123
this.leadingIcon = foundationMap.leadingIcon;
123124
this.helperText = foundationMap.helperText;
125+
this.ariaDescribedbyIds =
126+
this.adapter.getSelectAnchorAttr(strings.ARIA_DESCRIBEDBY)
127+
?.trim()
128+
?.split(' ') ||
129+
[];
124130
}
125131

126132
/** Returns the index of the currently selected menu item, or -1 if none. */
@@ -480,11 +486,20 @@ export class MDCSelectFoundation extends MDCFoundation<MDCSelectAdapter> {
480486
const helperTextId = this.helperText.getId();
481487

482488
if (helperTextVisible && helperTextId) {
483-
this.adapter.setSelectAnchorAttr(strings.ARIA_DESCRIBEDBY, helperTextId);
489+
this.adapter.setSelectAnchorAttr(
490+
strings.ARIA_DESCRIBEDBY, this.ariaDescribedbyIds.join(' '));
484491
} else {
485-
// Needed because screenreaders will read labels pointed to by
486-
// `aria-describedby` even if they are `aria-hidden`.
487-
this.adapter.removeSelectAnchorAttr(strings.ARIA_DESCRIBEDBY);
492+
// Remove helptext from list of describedby ids. Needed because
493+
// screenreaders will read labels pointed to by `aria-describedby` even if
494+
// they are `aria-hidden`.
495+
if (this.ariaDescribedbyIds.length > 1) {
496+
this.adapter.setSelectAnchorAttr(
497+
strings.ARIA_DESCRIBEDBY,
498+
this.ariaDescribedbyIds.filter(id => id !== helperTextId)
499+
.join(' '));
500+
} else { // helper text is the only describedby element
501+
this.adapter.removeSelectAnchorAttr(strings.ARIA_DESCRIBEDBY);
502+
}
488503
}
489504
}
490505

packages/mdc-select/test/foundation.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ describe('MDCSelectFoundation', () => {
8383
]);
8484
});
8585

86-
function setupTest(hasLeadingIcon = true, hasHelperText = false) {
86+
function setupTest(
87+
hasLeadingIcon = true, hasHelperText = false, describedbyElements = '') {
8788
const mockAdapter = createMockAdapter(MDCSelectFoundation);
8889
const leadingIcon = jasmine.createSpyObj('leadingIcon', [
8990
'setDisabled', 'setAriaLabel', 'setContent', 'registerInteractionHandler',
@@ -107,6 +108,8 @@ describe('MDCSelectFoundation', () => {
107108
mockAdapter.getMenuItemTextAtIndex.withArgs(0).and.returnValue('foo');
108109
mockAdapter.getMenuItemTextAtIndex.withArgs(1).and.returnValue('bar');
109110
mockAdapter.getMenuItemCount.and.returnValue(2);
111+
mockAdapter.getSelectAnchorAttr.withArgs('aria-describedby')
112+
.and.returnValue(describedbyElements);
110113

111114
const foundation = new MDCSelectFoundation(mockAdapter, foundationMap);
112115
return {foundation, mockAdapter, leadingIcon, helperText};
@@ -768,10 +771,10 @@ describe('MDCSelectFoundation', () => {
768771
() => {
769772
const hasIcon = false;
770773
const hasHelperText = true;
774+
const mockId = 'foobarbazcool';
771775
const {foundation, mockAdapter, helperText} =
772-
setupTest(hasIcon, hasHelperText);
776+
setupTest(hasIcon, hasHelperText, mockId);
773777

774-
const mockId = 'foobarbazcool';
775778
helperText.getId.and.returnValue(mockId);
776779
helperText.isVisible.and.returnValue(true);
777780

@@ -780,6 +783,59 @@ describe('MDCSelectFoundation', () => {
780783
.toHaveBeenCalledWith(strings.ARIA_DESCRIBEDBY, mockId);
781784
});
782785

786+
it('#setValid, with client ids, sets aria-describedby', () => {
787+
const hasIcon = false;
788+
const hasHelperText = true;
789+
const mockId = 'foobarbazcool';
790+
const clientDescribedbyIds = 'id1 id2 id3';
791+
792+
const {foundation, mockAdapter, helperText} =
793+
setupTest(hasIcon, hasHelperText, clientDescribedbyIds + ' ' + mockId);
794+
795+
helperText.getId.and.returnValue(mockId);
796+
helperText.isVisible.and.returnValue(true);
797+
798+
foundation.setValid(false);
799+
expect(mockAdapter.setSelectAnchorAttr)
800+
.toHaveBeenCalledWith(
801+
strings.ARIA_DESCRIBEDBY, clientDescribedbyIds + ' ' + mockId);
802+
});
803+
804+
it('#setValid, w/ client ids, remove helpertextId from aria-describedby',
805+
() => {
806+
const hasIcon = false;
807+
const hasHelperText = true;
808+
const mockId = 'foobarbazcool';
809+
const clientDescribedbyIds = `id1 id2 id3`;
810+
811+
const {foundation, mockAdapter, helperText} = setupTest(
812+
hasIcon, hasHelperText, clientDescribedbyIds + ' ' + mockId);
813+
814+
helperText.getId.and.returnValue(mockId);
815+
helperText.isVisible.and.returnValue(false);
816+
817+
foundation.setValid(false);
818+
expect(mockAdapter.setSelectAnchorAttr)
819+
.toHaveBeenCalledWith(
820+
strings.ARIA_DESCRIBEDBY, clientDescribedbyIds);
821+
});
822+
823+
it('#setValid, no client describedby ids, remove aria-describedby', () => {
824+
const hasIcon = false;
825+
const hasHelperText = true;
826+
const mockId = 'foobarbazcool';
827+
828+
const {foundation, mockAdapter, helperText} =
829+
setupTest(hasIcon, hasHelperText, mockId);
830+
831+
helperText.getId.and.returnValue(mockId);
832+
helperText.isVisible.and.returnValue(false);
833+
834+
foundation.setValid(false);
835+
expect(mockAdapter.removeSelectAnchorAttr)
836+
.toHaveBeenCalledWith(strings.ARIA_DESCRIBEDBY);
837+
});
838+
783839
it('#setValid true sets aria-invalid to false and removes invalid classes',
784840
() => {
785841
const {foundation, mockAdapter} = setupTest();

0 commit comments

Comments
 (0)