mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 10:01:59 +08:00
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:
@ -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
|
||||||
|
8
core/src/components.d.ts
vendored
8
core/src/components.d.ts
vendored
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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)}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
Reference in New Issue
Block a user