Skip to content

Commit 5acf8ab

Browse files
committed
feat(select): add helperText and errorText properties
1 parent efd3e0f commit 5acf8ab

File tree

7 files changed

+160
-2
lines changed

7 files changed

+160
-2
lines changed

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,10 @@ export namespace Components {
27552755
* If `true`, the user cannot interact with the select.
27562756
*/
27572757
"disabled": boolean;
2758+
/**
2759+
* Text that is placed under the select and displayed when an error is detected.
2760+
*/
2761+
"errorText"?: string;
27582762
/**
27592763
* 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.
27602764
*/
@@ -2763,6 +2767,10 @@ export namespace Components {
27632767
* 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.
27642768
*/
27652769
"fill"?: 'outline' | 'solid';
2770+
/**
2771+
* Text that is placed under the select and displayed when no error is detected.
2772+
*/
2773+
"helperText"?: string;
27662774
/**
27672775
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
27682776
*/
@@ -7568,6 +7576,10 @@ declare namespace LocalJSX {
75687576
* If `true`, the user cannot interact with the select.
75697577
*/
75707578
"disabled"?: boolean;
7579+
/**
7580+
* Text that is placed under the select and displayed when an error is detected.
7581+
*/
7582+
"errorText"?: string;
75717583
/**
75727584
* 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.
75737585
*/
@@ -7576,6 +7588,10 @@ declare namespace LocalJSX {
75767588
* 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.
75777589
*/
75787590
"fill"?: 'outline' | 'solid';
7591+
/**
7592+
* Text that is placed under the select and displayed when no error is detected.
7593+
*/
7594+
"helperText"?: string;
75797595
/**
75807596
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
75817597
*/

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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
* @prop --border-width: Width of the select border
2626
*
2727
* @prop --ripple-color: The color of the ripple effect on MD mode.
28+
*
29+
* TODO: add supporting text css vars
2830
*/
2931
--padding-top: 0px;
3032
--padding-end: 0px;
@@ -275,6 +277,71 @@ button {
275277
--highlight-color: var(--highlight-color-valid);
276278
}
277279

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

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
*/
@@ -983,13 +998,64 @@ export class Select implements ComponentInterface {
983998
aria-label={this.ariaLabel}
984999
aria-haspopup="dialog"
9851000
aria-expanded={`${isExpanded}`}
1001+
aria-describedby={this.getHintTextID()}
1002+
aria-invalid={this.getHintTextID() === this.errorTextId}
9861003
onFocus={this.onFocus}
9871004
onBlur={this.onBlur}
9881005
ref={(focusEl) => (this.focusEl = focusEl)}
9891006
></button>
9901007
);
9911008
}
9921009

1010+
private getHintTextID(): string | undefined {
1011+
const { el, helperText, errorText, helperTextId, errorTextId } = this;
1012+
1013+
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
1014+
return errorTextId;
1015+
}
1016+
1017+
if (helperText) {
1018+
return helperTextId;
1019+
}
1020+
1021+
return undefined;
1022+
}
1023+
1024+
/**
1025+
* Renders the helper text or error text values
1026+
*/
1027+
private renderHintText() {
1028+
const { helperText, errorText, helperTextId, errorTextId } = this;
1029+
1030+
return [
1031+
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
1032+
{helperText}
1033+
</div>,
1034+
<div id={errorTextId} class="error-text" part="supporting-text error-text">
1035+
{errorText}
1036+
</div>,
1037+
];
1038+
}
1039+
1040+
/**
1041+
* Responsible for rendering helper text, and error text. This element
1042+
* should only be rendered if hint text is set.
1043+
*/
1044+
private renderBottomContent() {
1045+
const { helperText, errorText } = this;
1046+
1047+
/**
1048+
* undefined and empty string values should
1049+
* be treated as not having helper/error text.
1050+
*/
1051+
const hasHintText = !!helperText || !!errorText;
1052+
if (!hasHintText) {
1053+
return;
1054+
}
1055+
1056+
return <div class="select-bottom">{this.renderHintText()}</div>;
1057+
}
1058+
9931059
render() {
9941060
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
9951061
this;
@@ -1069,6 +1135,7 @@ export class Select implements ComponentInterface {
10691135
{hasFloatingOrStackedLabel && this.renderSelectIcon()}
10701136
{shouldRenderHighlight && <div class="select-highlight"></div>}
10711137
</label>
1138+
{this.renderBottomContent()}
10721139
</Host>
10731140
);
10741141
}

packages/angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,15 +2060,15 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
20602060

20612061

20622062
@ProxyCmp({
2063-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
2063+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
20642064
methods: ['open']
20652065
})
20662066
@Component({
20672067
selector: 'ion-select',
20682068
changeDetection: ChangeDetectionStrategy.OnPush,
20692069
template: '<ng-content></ng-content>',
20702070
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2071-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
2071+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
20722072
})
20732073
export class IonSelect {
20742074
protected el: HTMLElement;

packages/vue/src/proxies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,8 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
771771
'compareWith',
772772
'disabled',
773773
'fill',
774+
'errorText',
775+
'helperText',
774776
'interface',
775777
'interfaceOptions',
776778
'justify',

0 commit comments

Comments
 (0)