Skip to content

Commit 0b54983

Browse files
feat(select): add required prop (#30155)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? - Currently, the screen reader do not announce the component as required when `required={true}`. ## What is the new behavior? - Added a new `required` prop to be used for accessibility purposes that adds the `aria-required` attribute to select's inner native button. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
1 parent 0bbb9f3 commit 0b54983

File tree

6 files changed

+53
-3
lines changed

6 files changed

+53
-3
lines changed

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,7 @@ ion-select,prop,multiple,boolean,false,false,false
16321632
ion-select,prop,name,string,this.inputId,false,false
16331633
ion-select,prop,okText,string,'OK',false,false
16341634
ion-select,prop,placeholder,string | undefined,undefined,false,false
1635+
ion-select,prop,required,boolean,false,false,false
16351636
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
16361637
ion-select,prop,shape,"round" | undefined,undefined,false,false
16371638
ion-select,prop,toggleIcon,string | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,6 +2812,10 @@ export namespace Components {
28122812
* The text to display when the select is empty.
28132813
*/
28142814
"placeholder"?: string;
2815+
/**
2816+
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
2817+
*/
2818+
"required": boolean;
28152819
/**
28162820
* The text to display instead of the selected option's value.
28172821
*/
@@ -7652,6 +7656,10 @@ declare namespace LocalJSX {
76527656
* The text to display when the select is empty.
76537657
*/
76547658
"placeholder"?: string;
7659+
/**
7660+
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
7661+
*/
7662+
"required"?: boolean;
76557663
/**
76567664
* The text to display instead of the selected option's value.
76577665
*/

core/src/components/select/select.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ export class Select implements ComponentInterface {
196196
*/
197197
@Prop({ mutable: true }) value?: any | null;
198198

199+
/**
200+
* If true, screen readers will announce it as a required field. This property
201+
* works only for accessibility purposes, it will not prevent the form from
202+
* submitting if the value is invalid.
203+
*/
204+
@Prop() required = false;
205+
199206
/**
200207
* Emitted when the value has changed.
201208
*
@@ -974,7 +981,7 @@ export class Select implements ComponentInterface {
974981
}
975982

976983
private renderListbox() {
977-
const { disabled, inputId, isExpanded } = this;
984+
const { disabled, inputId, isExpanded, required } = this;
978985

979986
return (
980987
<button
@@ -983,6 +990,7 @@ export class Select implements ComponentInterface {
983990
aria-label={this.ariaLabel}
984991
aria-haspopup="dialog"
985992
aria-expanded={`${isExpanded}`}
993+
aria-required={`${required}`}
986994
onFocus={this.onFocus}
987995
onBlur={this.onBlur}
988996
ref={(focusEl) => (this.focusEl = focusEl)}

core/src/components/select/test/select.spec.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,35 @@ describe('select: slot interactivity', () => {
125125
expect(divSpy).toHaveBeenCalled();
126126
});
127127
});
128+
129+
describe('ion-select: required', () => {
130+
it('should have a aria-required attribute as true in inner button', async () => {
131+
const page = await newSpecPage({
132+
components: [Select],
133+
html: `
134+
<ion-select required="true"></ion-select>
135+
`,
136+
});
137+
138+
const select = page.body.querySelector('ion-select')!;
139+
140+
const nativeButton = select.shadowRoot!.querySelector('button')!;
141+
142+
expect(nativeButton.getAttribute('aria-required')).toBe('true');
143+
});
144+
145+
it('should not have a aria-required attribute as false in inner button', async () => {
146+
const page = await newSpecPage({
147+
components: [Select],
148+
html: `
149+
<ion-select required="false"></ion-select>
150+
`,
151+
});
152+
153+
const select = page.body.querySelector('ion-select')!;
154+
155+
const nativeButton = select.shadowRoot!.querySelector('button')!;
156+
157+
expect(nativeButton.getAttribute('aria-required')).toBe('false');
158+
});
159+
});

packages/angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,15 +2060,15 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
20602060

20612061

20622062
@ProxyCmp({
2063-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
2063+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
20642064
methods: ['open']
20652065
})
20662066
@Component({
20672067
selector: 'ion-select',
20682068
changeDetection: ChangeDetectionStrategy.OnPush,
20692069
template: '<ng-content></ng-content>',
20702070
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2071-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
2071+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
20722072
})
20732073
export class IonSelect {
20742074
protected el: HTMLIonSelectElement;

packages/vue/src/proxies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,7 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
883883
'expandedIcon',
884884
'shape',
885885
'value',
886+
'required',
886887
'ionChange',
887888
'ionCancel',
888889
'ionDismiss',

0 commit comments

Comments
 (0)