Skip to content
Merged
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
16 changes: 14 additions & 2 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,9 @@ export namespace Components {
*/
"loadingText"?: string | IonicSafeString;
}
/**
* @experimental The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML.
*/
interface IonInput {
/**
* This attribute is ignored.
Expand Down Expand Up @@ -1214,7 +1217,7 @@ export namespace Components {
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* The visible label associated with the input.
* The visible label associated with the input. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used.
*/
"label"?: string;
/**
Expand Down Expand Up @@ -3591,6 +3594,9 @@ declare global {
prototype: HTMLIonInfiniteScrollContentElement;
new (): HTMLIonInfiniteScrollContentElement;
};
/**
* @experimental The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML.
*/
interface HTMLIonInputElement extends Components.IonInput, HTMLStencilElement {
}
var HTMLIonInputElement: {
Expand Down Expand Up @@ -5177,6 +5183,9 @@ declare namespace LocalJSX {
*/
"loadingText"?: string | IonicSafeString;
}
/**
* @experimental The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML.
*/
interface IonInput {
/**
* This attribute is ignored.
Expand Down Expand Up @@ -5248,7 +5257,7 @@ declare namespace LocalJSX {
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* The visible label associated with the input.
* The visible label associated with the input. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used.
*/
"label"?: string;
/**
Expand Down Expand Up @@ -7478,6 +7487,9 @@ declare module "@stencil/core" {
"ion-img": LocalJSX.IonImg & JSXBase.HTMLAttributes<HTMLIonImgElement>;
"ion-infinite-scroll": LocalJSX.IonInfiniteScroll & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollElement>;
"ion-infinite-scroll-content": LocalJSX.IonInfiniteScrollContent & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollContentElement>;
/**
* @experimental The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML.
*/
"ion-input": LocalJSX.IonInput & JSXBase.HTMLAttributes<HTMLIonInputElement>;
"ion-item": LocalJSX.IonItem & JSXBase.HTMLAttributes<HTMLIonItemElement>;
"ion-item-divider": LocalJSX.IonItemDivider & JSXBase.HTMLAttributes<HTMLIonItemDividerElement>;
Expand Down
12 changes: 11 additions & 1 deletion core/src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -463,14 +463,24 @@
* works on block-level elements. A flex item is
* considered blockified (https://www.w3.org/TR/css-display-3/#blockify).
*/
.label-text {
.label-text,
::slotted([slot="label"]) {
text-overflow: ellipsis;

white-space: nowrap;

overflow: hidden;
}

/**
* If no label text is placed into the slot
* then the element should be hidden otherwise
* there will be additional margins added.
*/
.label-text-wrapper-hidden {
display: none;
}

.input-wrapper input {
/**
* When the floating label appears on top of the
Expand Down
36 changes: 31 additions & 5 deletions core/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { getCounterText } from './input.utils';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot label - @experimental The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML.
*/
@Component({
tag: 'ion-input',
Expand Down Expand Up @@ -165,6 +167,10 @@ export class Input implements ComponentInterface {

/**
* The visible label associated with the input.
*
* Use this if you need to render a plaintext label.
*
* The `label` property will take priority over the `label` slot if both are used.
*/
@Prop() label?: string;

Expand Down Expand Up @@ -578,17 +584,37 @@ export class Input implements ComponentInterface {

private renderLabel() {
const { label } = this;
if (label === undefined) {
return;
}

return (
<div class="label-text-wrapper">
<div class="label-text">{this.label}</div>
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': !this.hasLabel,
}}
>
{label === undefined ? <slot name="label"></slot> : <div class="label-text">{label}</div>}
</div>
);
}

/**
* Gets any content passed into the `label` slot,
* not the <slot> definition.
*/
private get labelSlot() {
return this.el.querySelector('[slot="label"]');
}

/**
* Returns `true` if label content is provided
* either by a prop or a content. If you want
* to get the plaintext value of the label use
* the `labelText` getter instead.
*/
private get hasLabel() {
return this.label !== undefined || this.labelSlot !== null;
}

/**
* Renders the border container
* when fill="outline".
Expand Down
1 change: 1 addition & 0 deletions core/src/components/input/test/a11y/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<main>
<h1>Input - a11y</h1>

<ion-input><div slot="label">Slotted Label</div></ion-input><br />
<ion-input label="my label"></ion-input><br />
<ion-input aria-label="my aria label"></ion-input><br />
<ion-input label="Email" label-placement="stacked" value="[email protected]"></ion-input><br />
Expand Down
54 changes: 54 additions & 0 deletions core/src/components/input/test/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,57 @@ describe('input: rendering', () => {
expect(bottomContent).toBe(null);
});
});

/**
* Input uses emulated slots, so the internal
* behavior will not exactly match IonSelect's slots.
* For example, Input does not render an actual `<slot>` element
* internally, so we do not check for that here. Instead,
* we check to see which label text is being used.
* If Input is updated to use Shadow DOM (and therefore native slots),
* then we can update these tests to more closely match the Select tests.
**/
describe('input: label rendering', () => {
it('should render label prop if only prop provided', async () => {
const page = await newSpecPage({
components: [Input],
html: `
<ion-input label="Label Prop Text"></ion-input>
`,
});

const input = page.body.querySelector('ion-input');

const labelText = input.querySelector('.label-text-wrapper');

expect(labelText.textContent).toBe('Label Prop Text');
});
it('should render label slot if only slot provided', async () => {
const page = await newSpecPage({
components: [Input],
html: `
<ion-input><div slot="label">Label Prop Slot</div></ion-input>
`,
});

const input = page.body.querySelector('ion-input');

const labelText = input.querySelector('.label-text-wrapper');

expect(labelText.textContent).toBe('Label Prop Slot');
});
it('should render label prop if both prop and slot provided', async () => {
const page = await newSpecPage({
components: [Input],
html: `
<ion-input label="Label Prop Text"><div slot="label">Label Prop Slot</div></ion-input>
`,
});

const input = page.body.querySelector('ion-input');

const labelText = input.querySelector('.label-text-wrapper');

expect(labelText.textContent).toBe('Label Prop Text');
});
});
50 changes: 29 additions & 21 deletions core/src/components/input/test/label-placement/input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,6 @@ configs().forEach(({ title, screenshot, config }) => {
const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-placement-start`));
});

test('long label should truncate', async ({ page }) => {
await page.setContent(
`
<ion-input label="Email Email Email Email Email Email Email Email Email Email Email Email" value="[email protected]" label-placement="start"></ion-input>
`,
config
);
const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-placement-start-long-label`));
});
});
test.describe(title('input: label placement end'), () => {
test('label should appear on the ending side of the input', async ({ page }) => {
Expand All @@ -38,16 +27,6 @@ configs().forEach(({ title, screenshot, config }) => {
const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-placement-end`));
});
test('long label should truncate', async ({ page }) => {
await page.setContent(
`
<ion-input label="Email Email Email Email Email Email Email Email Email Email Email Email" value="[email protected]" label-placement="end"></ion-input>
`,
config
);
const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-placement-end-long-label`));
});
});
test.describe(title('input: label placement fixed'), () => {
test('label should appear on the starting side of the input, have a fixed width, and show ellipses', async ({
Expand Down Expand Up @@ -179,3 +158,32 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
});
});
});

configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input: label overflow'), () => {
test('label property should be truncated with ellipses', async ({ page }) => {
await page.setContent(
`
<ion-input label="Label Label Label Label Label" placeholder="Text Input"></ion-input>
`,
config
);

const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-label-truncate`));
});
test('label slot should be truncated with ellipses', async ({ page }) => {
await page.setContent(
`
<ion-input placeholder="Text Input">
<div slot="label">Label Label Label Label Label</div>
</ion-input>
`,
config
);

const input = page.locator('ion-input');
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-label-slot-truncate`));
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
98 changes: 98 additions & 0 deletions core/src/components/input/test/slot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - Slot</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;
}
}

.required {
color: red;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - Slot</ion-title>
</ion-toolbar>
</ion-header>

<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>No Fill / Start</h2>
<ion-input label-placement="start" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>

<div class="grid-item">
<h2>Solid / Start</h2>
<ion-input label-placement="start" fill="solid" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>

<div class="grid-item">
<h2>Outline / Start</h2>
<ion-input label-placement="start" fill="outline" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>

<div class="grid-item">
<h2>No Fill / Floating</h2>
<ion-input label-placement="floating" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>

<div class="grid-item">
<h2>Solid / Floating</h2>
<ion-input label-placement="floating" fill="solid" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>

<div class="grid-item">
<h2>Outline / Floating</h2>
<ion-input label-placement="floating" fill="outline" value="[email protected]">
<div slot="label">Email <span class="required">*</span></div>
</ion-input>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>
2 changes: 1 addition & 1 deletion core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot label - The label text to associate with the select. Use the "labelPlacement" property to control where the label is placed relative to the select. Use this if you need to render a label with custom HTML.
* @slot label - The label text to associate with the select. Use the `labelPlacement` property to control where the label is placed relative to the select. Use this if you need to render a label with custom HTML.
*
* @part placeholder - The text displayed in the select when there is no value.
* @part text - The displayed value of the select.
Expand Down