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. -->
This commit is contained in:
Giuliana Silva
2025-02-04 10:18:40 +00:00
committed by GitHub
parent 0bbb9f37b4
commit 0b549835b6
6 changed files with 53 additions and 3 deletions

View File

@ -1632,6 +1632,7 @@ ion-select,prop,multiple,boolean,false,false,false
ion-select,prop,name,string,this.inputId,false,false ion-select,prop,name,string,this.inputId,false,false
ion-select,prop,okText,string,'OK',false,false ion-select,prop,okText,string,'OK',false,false
ion-select,prop,placeholder,string | undefined,undefined,false,false ion-select,prop,placeholder,string | undefined,undefined,false,false
ion-select,prop,required,boolean,false,false,false
ion-select,prop,selectedText,null | string | undefined,undefined,false,false ion-select,prop,selectedText,null | string | undefined,undefined,false,false
ion-select,prop,shape,"round" | undefined,undefined,false,false ion-select,prop,shape,"round" | undefined,undefined,false,false
ion-select,prop,toggleIcon,string | undefined,undefined,false,false ion-select,prop,toggleIcon,string | undefined,undefined,false,false

View File

@ -2812,6 +2812,10 @@ export namespace Components {
* The text to display when the select is empty. * The text to display when the select is empty.
*/ */
"placeholder"?: string; "placeholder"?: string;
/**
* 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.
*/
"required": boolean;
/** /**
* The text to display instead of the selected option's value. * The text to display instead of the selected option's value.
*/ */
@ -7652,6 +7656,10 @@ declare namespace LocalJSX {
* The text to display when the select is empty. * The text to display when the select is empty.
*/ */
"placeholder"?: string; "placeholder"?: string;
/**
* 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.
*/
"required"?: boolean;
/** /**
* The text to display instead of the selected option's value. * The text to display instead of the selected option's value.
*/ */

View File

@ -196,6 +196,13 @@ export class Select implements ComponentInterface {
*/ */
@Prop({ mutable: true }) value?: any | null; @Prop({ mutable: true }) value?: any | null;
/**
* 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.
*/
@Prop() required = false;
/** /**
* Emitted when the value has changed. * Emitted when the value has changed.
* *
@ -974,7 +981,7 @@ export class Select implements ComponentInterface {
} }
private renderListbox() { private renderListbox() {
const { disabled, inputId, isExpanded } = this; const { disabled, inputId, isExpanded, required } = this;
return ( return (
<button <button
@ -983,6 +990,7 @@ export class Select implements ComponentInterface {
aria-label={this.ariaLabel} aria-label={this.ariaLabel}
aria-haspopup="dialog" aria-haspopup="dialog"
aria-expanded={`${isExpanded}`} aria-expanded={`${isExpanded}`}
aria-required={`${required}`}
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
ref={(focusEl) => (this.focusEl = focusEl)} ref={(focusEl) => (this.focusEl = focusEl)}

View File

@ -125,3 +125,35 @@ describe('select: slot interactivity', () => {
expect(divSpy).toHaveBeenCalled(); expect(divSpy).toHaveBeenCalled();
}); });
}); });
describe('ion-select: required', () => {
it('should have a aria-required attribute as true in inner button', async () => {
const page = await newSpecPage({
components: [Select],
html: `
<ion-select required="true"></ion-select>
`,
});
const select = page.body.querySelector('ion-select')!;
const nativeButton = select.shadowRoot!.querySelector('button')!;
expect(nativeButton.getAttribute('aria-required')).toBe('true');
});
it('should not have a aria-required attribute as false in inner button', async () => {
const page = await newSpecPage({
components: [Select],
html: `
<ion-select required="false"></ion-select>
`,
});
const select = page.body.querySelector('ion-select')!;
const nativeButton = select.shadowRoot!.querySelector('button')!;
expect(nativeButton.getAttribute('aria-required')).toBe('false');
});
});

View File

@ -2060,7 +2060,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
@ProxyCmp({ @ProxyCmp({
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
methods: ['open'] methods: ['open']
}) })
@Component({ @Component({
@ -2068,7 +2068,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>', template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
}) })
export class IonSelect { export class IonSelect {
protected el: HTMLIonSelectElement; protected el: HTMLIonSelectElement;

View File

@ -883,6 +883,7 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
'expandedIcon', 'expandedIcon',
'shape', 'shape',
'value', 'value',
'required',
'ionChange', 'ionChange',
'ionCancel', 'ionCancel',
'ionDismiss', 'ionDismiss',