Skip to content

Commit

Permalink
feat(select): add helperText and errorText properties
Browse files Browse the repository at this point in the history
  • Loading branch information
brandyscarney committed Jan 21, 2025
1 parent efd3e0f commit 5acf8ab
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 2 deletions.
16 changes: 16 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,10 @@ export namespace Components {
* If `true`, the user cannot interact with the select.
*/
"disabled": boolean;
/**
* Text that is placed under the select and displayed when an error is detected.
*/
"errorText"?: string;
/**
* 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.
*/
Expand All @@ -2763,6 +2767,10 @@ export namespace Components {
* 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.
*/
"fill"?: 'outline' | 'solid';
/**
* Text that is placed under the select and displayed when no error is detected.
*/
"helperText"?: string;
/**
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
Expand Down Expand Up @@ -7568,6 +7576,10 @@ declare namespace LocalJSX {
* If `true`, the user cannot interact with the select.
*/
"disabled"?: boolean;
/**
* Text that is placed under the select and displayed when an error is detected.
*/
"errorText"?: string;
/**
* 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.
*/
Expand All @@ -7576,6 +7588,10 @@ declare namespace LocalJSX {
* 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.
*/
"fill"?: 'outline' | 'solid';
/**
* Text that is placed under the select and displayed when no error is detected.
*/
"helperText"?: string;
/**
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/select/select.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// --------------------------------------------------

:host {
--border-width: #{$hairlines-width};
--border-color: #{$item-ios-border-color};
--highlight-height: 0px;
}

Expand Down
4 changes: 4 additions & 0 deletions core/src/components/select/select.md.solid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
--border-color: var(--highlight-color);
}

/**
* The bottom content should never have
* a border with the solid style.
*/
:host(.select-fill-solid) .select-bottom {
border-top: none;
}
Expand Down
67 changes: 67 additions & 0 deletions core/src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* @prop --border-width: Width of the select border
*
* @prop --ripple-color: The color of the ripple effect on MD mode.
*
* TODO: add supporting text css vars
*/
--padding-top: 0px;
--padding-end: 0px;
Expand Down Expand Up @@ -275,6 +277,71 @@ button {
--highlight-color: var(--highlight-color-valid);
}

// Select Bottom Content
// ----------------------------------------------------------------

.select-bottom {
/**
* The bottom content should take on the start and end
* padding so it is always aligned with either the label
* or the start of the text select.
*/
@include padding(5px, var(--padding-end), 0, var(--padding-start));

display: flex;

justify-content: space-between;

border-top: var(--border-width) var(--border-style) var(--border-color);

font-size: dynamic-font(12px);

white-space: normal;
}

/**
* If the select has a validity state, the
* border and label should reflect that as a color.
* The invalid state should show if the select is
* invalid and has already been touched.
* The valid state should show if the select
* is valid, has already been touched, and
* is currently focused. Do not show the valid
* highlight when the select is blurred.
*/
:host(.has-focus.ion-valid),
:host(.ion-touched.ion-invalid) {
--border-color: var(--highlight-color);
}

// Select Hint Text
// ----------------------------------------------------------------

/**
* Error text should only be shown when .ion-invalid is
* present on the select. Otherwise the helper text should
* be shown.
*/
.select-bottom .error-text {
display: none;

color: var(--highlight-color-invalid);
}

.select-bottom .helper-text {
display: block;

color: $text-color-step-300;
}

:host(.ion-touched.ion-invalid) .select-bottom .error-text {
display: block;
}

:host(.ion-touched.ion-invalid) .select-bottom .helper-text {
display: none;
}

// Select Label
// ----------------------------------------------------------------

Expand Down
67 changes: 67 additions & 0 deletions core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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;
Expand Down Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -983,13 +998,64 @@ 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}
onFocus={this.onFocus}
onBlur={this.onBlur}
ref={(focusEl) => (this.focusEl = focusEl)}
></button>
);
}

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;
Expand Down Expand Up @@ -1069,6 +1135,7 @@ export class Select implements ComponentInterface {
{hasFloatingOrStackedLabel && this.renderSelectIcon()}
{shouldRenderHighlight && <div class="select-highlight"></div>}
</label>
{this.renderBottomContent()}
</Host>
);
}
Expand Down
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', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', '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', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
})
export class IonSelect {
protected el: HTMLElement;
Expand Down
2 changes: 2 additions & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,8 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
'compareWith',
'disabled',
'fill',
'errorText',
'helperText',
'interface',
'interfaceOptions',
'justify',
Expand Down

0 comments on commit 5acf8ab

Please sign in to comment.