Skip to content
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,7 @@ ion-select,prop,multiple,boolean,false,false,false
ion-select,prop,name,string,this.inputId,false,false
ion-select,prop,okText,string,'OK',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,shape,"round" | undefined,undefined,false,false
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
Expand Down
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2808,6 +2808,10 @@ export namespace Components {
* The text to display when the select is empty.
*/
"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.
*/
Expand Down Expand Up @@ -7640,6 +7644,10 @@ declare namespace LocalJSX {
* The text to display when the select is empty.
*/
"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.
*/
Expand Down
10 changes: 9 additions & 1 deletion core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ export class Select implements ComponentInterface {
*/
@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.
*
Expand Down Expand Up @@ -974,7 +981,7 @@ export class Select implements ComponentInterface {
}

private renderListbox() {
const { disabled, inputId, isExpanded } = this;
const { disabled, inputId, isExpanded, required } = this;

return (
<button
Expand All @@ -983,6 +990,7 @@ export class Select implements ComponentInterface {
aria-label={this.ariaLabel}
aria-haspopup="dialog"
aria-expanded={`${isExpanded}`}
aria-required={`${required}`}
onFocus={this.onFocus}
onBlur={this.onBlur}
ref={(focusEl) => (this.focusEl = focusEl)}
Expand Down
32 changes: 32 additions & 0 deletions core/src/components/select/test/select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,35 @@ describe('select: slot interactivity', () => {
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');
});
});
4 changes: 2 additions & 2 deletions packages/angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2060,15 +2060,15 @@ export declare interface IonSegmentView extends Components.IonSegmentView {


@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']
})
@Component({
selector: 'ion-select',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// 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 {
protected el: HTMLIonSelectElement;
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
'expandedIcon',
'shape',
'value',
'required',
'ionChange',
'ionCancel',
'ionDismiss',
Expand Down
Loading