Skip to content

Commit bbdaec0

Browse files
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 <[email protected]> --------- Co-authored-by: Brandy Smith <[email protected]>
1 parent 94ca2e5 commit bbdaec0

File tree

50 files changed

+521
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+521
-2
lines changed

core/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,8 +1626,10 @@ ion-select,prop,cancelText,string,'Cancel',false,false
16261626
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
16271627
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
16281628
ion-select,prop,disabled,boolean,false,false,false
1629+
ion-select,prop,errorText,string | undefined,undefined,false,false
16291630
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
16301631
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
1632+
ion-select,prop,helperText,string | undefined,undefined,false,false
16311633
ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false
16321634
ion-select,prop,interfaceOptions,any,{},false,false
16331635
ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
@@ -1682,9 +1684,12 @@ ion-select,css-prop,--placeholder-opacity,md
16821684
ion-select,css-prop,--ripple-color,ios
16831685
ion-select,css-prop,--ripple-color,md
16841686
ion-select,part,container
1687+
ion-select,part,error-text
1688+
ion-select,part,helper-text
16851689
ion-select,part,icon
16861690
ion-select,part,label
16871691
ion-select,part,placeholder
1692+
ion-select,part,supporting-text
16881693
ion-select,part,text
16891694

16901695
ion-select-modal,scoped

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2771,6 +2771,10 @@ export namespace Components {
27712771
* If `true`, the user cannot interact with the select.
27722772
*/
27732773
"disabled": boolean;
2774+
/**
2775+
* Text that is placed under the select and displayed when an error is detected.
2776+
*/
2777+
"errorText"?: string;
27742778
/**
27752779
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
27762780
*/
@@ -2779,6 +2783,10 @@ export namespace Components {
27792783
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
27802784
*/
27812785
"fill"?: 'outline' | 'solid';
2786+
/**
2787+
* Text that is placed under the select and displayed when no error is detected.
2788+
*/
2789+
"helperText"?: string;
27822790
/**
27832791
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
27842792
*/
@@ -7616,6 +7624,10 @@ declare namespace LocalJSX {
76167624
* If `true`, the user cannot interact with the select.
76177625
*/
76187626
"disabled"?: boolean;
7627+
/**
7628+
* Text that is placed under the select and displayed when an error is detected.
7629+
*/
7630+
"errorText"?: string;
76197631
/**
76207632
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
76217633
*/
@@ -7624,6 +7636,10 @@ declare namespace LocalJSX {
76247636
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
76257637
*/
76267638
"fill"?: 'outline' | 'solid';
7639+
/**
7640+
* Text that is placed under the select and displayed when no error is detected.
7641+
*/
7642+
"helperText"?: string;
76277643
/**
76287644
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
76297645
*/

core/src/components/select/select.ios.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// --------------------------------------------------
66

77
:host {
8+
--border-width: #{$hairlines-width};
9+
--border-color: #{$item-ios-border-color};
810
--highlight-height: 0px;
911
}
1012

core/src/components/select/select.md.solid.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
--border-color: var(--highlight-color);
3333
}
3434

35+
/**
36+
* The bottom content should never have
37+
* a border with the solid style.
38+
*/
3539
:host(.select-fill-solid) .select-bottom {
3640
border-top: none;
3741
}

core/src/components/select/select.scss

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,71 @@ button {
275275
--highlight-color: var(--highlight-color-valid);
276276
}
277277

278+
// Select Bottom Content
279+
// ----------------------------------------------------------------
280+
281+
.select-bottom {
282+
/**
283+
* The bottom content should take on the start and end
284+
* padding so it is always aligned with either the label
285+
* or the start of the text select.
286+
*/
287+
@include padding(5px, var(--padding-end), 0, var(--padding-start));
288+
289+
display: flex;
290+
291+
justify-content: space-between;
292+
293+
border-top: var(--border-width) var(--border-style) var(--border-color);
294+
295+
font-size: dynamic-font(12px);
296+
297+
white-space: normal;
298+
}
299+
300+
/**
301+
* If the select has a validity state, the
302+
* border and label should reflect that as a color.
303+
* The invalid state should show if the select is
304+
* invalid and has already been touched.
305+
* The valid state should show if the select
306+
* is valid, has already been touched, and
307+
* is currently focused. Do not show the valid
308+
* highlight when the select is blurred.
309+
*/
310+
:host(.has-focus.ion-valid),
311+
:host(.ion-touched.ion-invalid) {
312+
--border-color: var(--highlight-color);
313+
}
314+
315+
// Select Hint Text
316+
// ----------------------------------------------------------------
317+
318+
/**
319+
* Error text should only be shown when .ion-invalid is
320+
* present on the select. Otherwise the helper text should
321+
* be shown.
322+
*/
323+
.select-bottom .error-text {
324+
display: none;
325+
326+
color: var(--highlight-color-invalid);
327+
}
328+
329+
.select-bottom .helper-text {
330+
display: block;
331+
332+
color: $text-color-step-300;
333+
}
334+
335+
:host(.ion-touched.ion-invalid) .select-bottom .error-text {
336+
display: block;
337+
}
338+
339+
:host(.ion-touched.ion-invalid) .select-bottom .helper-text {
340+
display: none;
341+
}
342+
278343
// Select Label
279344
// ----------------------------------------------------------------
280345

core/src/components/select/select.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
4141
* @part icon - The select icon container.
4242
* @part container - The container for the selected text or placeholder.
4343
* @part label - The label text describing the select.
44+
* @part supporting-text - Supporting text displayed beneath the select.
45+
* @part helper-text - Supporting text displayed beneath the select when the select is valid.
46+
* @part error-text - Supporting text displayed beneath the select when the select is invalid and touched.
4447
*/
4548
@Component({
4649
tag: 'ion-select',
@@ -52,6 +55,8 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
5255
})
5356
export class Select implements ComponentInterface {
5457
private inputId = `ion-sel-${selectIds++}`;
58+
private helperTextId = `${this.inputId}-helper-text`;
59+
private errorTextId = `${this.inputId}-error-text`;
5560
private overlay?: OverlaySelect;
5661
private focusEl?: HTMLButtonElement;
5762
private mutationO?: MutationObserver;
@@ -98,6 +103,16 @@ export class Select implements ComponentInterface {
98103
*/
99104
@Prop() fill?: 'outline' | 'solid';
100105

106+
/**
107+
* Text that is placed under the select and displayed when an error is detected.
108+
*/
109+
@Prop() errorText?: string;
110+
111+
/**
112+
* Text that is placed under the select and displayed when no error is detected.
113+
*/
114+
@Prop() helperText?: string;
115+
101116
/**
102117
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
103118
*/
@@ -1014,6 +1029,8 @@ export class Select implements ComponentInterface {
10141029
aria-label={this.ariaLabel}
10151030
aria-haspopup="dialog"
10161031
aria-expanded={`${isExpanded}`}
1032+
aria-describedby={this.getHintTextID()}
1033+
aria-invalid={this.getHintTextID() === this.errorTextId}
10171034
aria-required={`${required}`}
10181035
onFocus={this.onFocus}
10191036
onBlur={this.onBlur}
@@ -1022,6 +1039,55 @@ export class Select implements ComponentInterface {
10221039
);
10231040
}
10241041

1042+
private getHintTextID(): string | undefined {
1043+
const { el, helperText, errorText, helperTextId, errorTextId } = this;
1044+
1045+
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
1046+
return errorTextId;
1047+
}
1048+
1049+
if (helperText) {
1050+
return helperTextId;
1051+
}
1052+
1053+
return undefined;
1054+
}
1055+
1056+
/**
1057+
* Renders the helper text or error text values
1058+
*/
1059+
private renderHintText() {
1060+
const { helperText, errorText, helperTextId, errorTextId } = this;
1061+
1062+
return [
1063+
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
1064+
{helperText}
1065+
</div>,
1066+
<div id={errorTextId} class="error-text" part="supporting-text error-text">
1067+
{errorText}
1068+
</div>,
1069+
];
1070+
}
1071+
1072+
/**
1073+
* Responsible for rendering helper text, and error text. This element
1074+
* should only be rendered if hint text is set.
1075+
*/
1076+
private renderBottomContent() {
1077+
const { helperText, errorText } = this;
1078+
1079+
/**
1080+
* undefined and empty string values should
1081+
* be treated as not having helper/error text.
1082+
*/
1083+
const hasHintText = !!helperText || !!errorText;
1084+
if (!hasHintText) {
1085+
return;
1086+
}
1087+
1088+
return <div class="select-bottom">{this.renderHintText()}</div>;
1089+
}
1090+
10251091
render() {
10261092
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
10271093
this;
@@ -1101,6 +1167,7 @@ export class Select implements ComponentInterface {
11011167
{hasFloatingOrStackedLabel && this.renderSelectIcon()}
11021168
{shouldRenderHighlight && <div class="select-highlight"></div>}
11031169
</label>
1170+
{this.renderBottomContent()}
11041171
</Host>
11051172
);
11061173
}

0 commit comments

Comments
 (0)