Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select helper text #30065

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1619,8 +1619,10 @@ ion-select,prop,cancelText,string,'Cancel',false,false
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
ion-select,prop,disabled,boolean,false,false,false
ion-select,prop,errorText,string | undefined,undefined,false,false
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
ion-select,prop,helperText,string | undefined,undefined,false,false
ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false
ion-select,prop,interfaceOptions,any,{},false,false
ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
Expand Down
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
28 changes: 28 additions & 0 deletions core/src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,34 @@ button {
display: none;
}

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

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

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

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

color: #{$text-color-step-450};
}

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

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

// Select Native Wrapper
// ----------------------------------------------------------------

Expand Down
48 changes: 48 additions & 0 deletions core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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 +100,16 @@ export class Select implements ComponentInterface {
*/
@Prop() fill?: 'outline' | 'solid';

/**
* Text that is placed under the select and displayed when no error is detected.
*/
@Prop() helperText?: string;

/**
* Text that is placed under the select and displayed when an error is detected.
*/
@Prop() errorText?: string;

/**
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
Expand Down Expand Up @@ -714,6 +726,21 @@ export class Select implements ComponentInterface {
return this.getText() !== '';
}

/**
* Renders the helper text or error text values
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId } = this;

return [
<div id={helperTextId} class="helper-text">
{helperText}
</div>,
<div id={errorTextId} class="error-text">
{errorText}
</div>,
];
}
private get childOpts() {
return Array.from(this.el.querySelectorAll('ion-select-option'));
}
Expand Down Expand Up @@ -812,6 +839,26 @@ export class Select implements ComponentInterface {
this.ionBlur.emit();
};

/**
* 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="input-bottom">{this.renderHintText()}</div>;
}

private renderLabel() {
const { label } = this;

Expand Down Expand Up @@ -1069,6 +1116,7 @@ export class Select implements ComponentInterface {
{hasFloatingOrStackedLabel && this.renderSelectIcon()}
{shouldRenderHighlight && <div class="select-highlight"></div>}
</label>
{this.renderBottomContent()}
</Host>
);
}
Expand Down
99 changes: 99 additions & 0 deletions core/src/components/select/test/bottom-content/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - Bottom Content</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;

color: #6f7378;

margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}

ion-input.custom-error-color {
--highlight-color-invalid: purple;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>select - Bottom Content</ion-title>
</ion-toolbar>
</ion-header>

<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Select with Helper text</h2>
<ion-select class="ion-touched ion-valid" helper-text="Select a fruit" value="apple">
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
</div>

<div class="grid-item">
<h2>Select with Error text</h2>
<ion-select class="ion-touched ion-invalid" error-text="No fruit selected">
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
</div>

<div class="grid-item">
<h2>Select with Helper and error text| Valid</h2>
<ion-select
class="ion-touched ion-valid"
helper-text="Select a fruit"
error-text="No fruit selected"
value="apple"
>
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
</div>

<div class="grid-item">
<h2>Select with no helper or error text</h2>
<ion-select class="ion-touched ion-valid" value="bananna">
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
</div>
</div>

<script></script>
</ion-content>
</ion-app>
</body>
</html>
41 changes: 41 additions & 0 deletions core/src/components/select/test/bottom-content/select.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';

configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('Select: Helper and Error Text'), () => {
test.describe('Select with helper text', () => {
test('should set label and show helper text', async ({ page }) => {
await page.setContent(
`
<ion-select class="ion-touched ion-valid" helper-text="Select a fruit" value="apple">
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
`,
config
);

const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-helper-text`));
});
});
test.describe('Select with Error text', () => {
test('should set label and show error text', async ({ page }) => {
await page.setContent(
`
<ion-select class="ion-touched ion-invalid" error-text="No fruit selected">
<div slot="label">Favorite Fruit</div>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="bananna">Bananna</ion-select-option>
</ion-select>
`,
config
);

const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-error-text`));
});
});
});
});
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',
'helperText',
'errorText',
'interface',
'interfaceOptions',
'justify',
Expand Down
Loading