feat(select): add helperText and errorText properties (#30143)

Issue number: resolves #29205

---------

## What is the current behavior?
Select does not support helper and error text.

## What is the new behavior?
- Adds support for `helperText` and `errorText`
- Adds parts for `helper-text`, `error-text` and `supporting-text`
- Adds an e2e test for helper and error text with functional tests and
screenshot tests

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information


[Preview](https://ionic-framework-git-rou-11551-ionic1.vercel.app/src/components/select/test/bottom-content)

---------

Co-authored-by: swimer11 <65334157+swimer11@users.noreply.github.com>

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-03-10 18:53:11 -04:00
committed by GitHub
parent 94ca2e54cb
commit bbdaec0cc1
50 changed files with 521 additions and 2 deletions

View File

@@ -41,6 +41,9 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
* @part icon - The select icon container.
* @part container - The container for the selected text or placeholder.
* @part label - The label text describing the select.
* @part supporting-text - Supporting text displayed beneath the select.
* @part helper-text - Supporting text displayed beneath the select when the select is valid.
* @part error-text - Supporting text displayed beneath the select when the select is invalid and touched.
*/
@Component({
tag: 'ion-select',
@@ -52,6 +55,8 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
})
export class Select implements ComponentInterface {
private inputId = `ion-sel-${selectIds++}`;
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
private overlay?: OverlaySelect;
private focusEl?: HTMLButtonElement;
private mutationO?: MutationObserver;
@@ -98,6 +103,16 @@ export class Select implements ComponentInterface {
*/
@Prop() fill?: 'outline' | 'solid';
/**
* Text that is placed under the select and displayed when an error is detected.
*/
@Prop() errorText?: string;
/**
* Text that is placed under the select and displayed when no error is detected.
*/
@Prop() helperText?: string;
/**
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
@@ -1014,6 +1029,8 @@ export class Select implements ComponentInterface {
aria-label={this.ariaLabel}
aria-haspopup="dialog"
aria-expanded={`${isExpanded}`}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
aria-required={`${required}`}
onFocus={this.onFocus}
onBlur={this.onBlur}
@@ -1022,6 +1039,55 @@ export class Select implements ComponentInterface {
);
}
private getHintTextID(): string | undefined {
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
if (helperText) {
return helperTextId;
}
return undefined;
}
/**
* Renders the helper text or error text values
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId } = this;
return [
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
{helperText}
</div>,
<div id={errorTextId} class="error-text" part="supporting-text error-text">
{errorText}
</div>,
];
}
/**
* Responsible for rendering helper text, and error text. This element
* should only be rendered if hint text is set.
*/
private renderBottomContent() {
const { helperText, errorText } = this;
/**
* undefined and empty string values should
* be treated as not having helper/error text.
*/
const hasHintText = !!helperText || !!errorText;
if (!hasHintText) {
return;
}
return <div class="select-bottom">{this.renderHintText()}</div>;
}
render() {
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
this;
@@ -1101,6 +1167,7 @@ export class Select implements ComponentInterface {
{hasFloatingOrStackedLabel && this.renderSelectIcon()}
{shouldRenderHighlight && <div class="select-highlight"></div>}
</label>
{this.renderBottomContent()}
</Host>
);
}