refactor(input): remove legacy property and support for legacy syntax (#29017)

Issue number: internal

---------

## What is the current behavior?

In Ionic Framework v7, we [simplified the input
syntax](https://ionic.io/blog/ionic-7-is-here#simplified-form-control-syntax)
so that it was no longer required to be placed inside of an `ion-item`.
We maintained backwards compatibility by adding a `legacy` property
which allowed it to continue to be styled properly when written in the
following way:

```html
<ion-item>
  <ion-label>Label</ion-label>
  <ion-input></ion-input>
</ion-item>
```

While this was supported in v7, console warnings were logged to notify
developers that they needed to update this syntax for the best
accessibility experience.

## What is the new behavior?

- Removes the `legacy` property and support for the legacy syntax.
Developers should follow the [migration
guide](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax)
in the input documentation to update their apps. The new syntax requires
a `label` or `aria-label` on `ion-input`:
    ```html
    <ion-item>
      <ion-input label="Label"></ion-input>
    </ion-item>
    ```
- Removes the legacy tests under under `input/test/legacy/` and all
related screenshots
- Removes the input usage from `item/test/a11y`, `item/test/counter`,
`item/test/disabled`, `item/test/highlight`,
`item/test/legacy/alignment`, `item/test/legacy/disabled`,
`item/test/legacy/fill`, and `item/test/legacy/form` and all related
screenshots if the test was removed

## Does this introduce a breaking change?

- [x] Yes
- [ ] No

1. Developers have had console warnings when using the legacy syntax
since the v7 release. The migration guide for the new input syntax is
outlined in the [Input
documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).
2. This change has been documented in the Breaking Changes document with
a link to the migration guide.

BREAKING CHANGE:

The `legacy` property and support for the legacy syntax, which involved
placing an `ion-input` inside of an `ion-item` with an `ion-label`, have
been removed from input. For more information on migrating from the
legacy input syntax, refer to the [Input
documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).

---------

Co-authored-by: ionitron <hi@ionicframework.com>
This commit is contained in:
Brandy Carney
2024-02-13 12:43:22 -05:00
committed by GitHub
parent ca61e5061b
commit 76abf2778b
246 changed files with 9 additions and 2868 deletions

View File

@@ -154,6 +154,7 @@ For more information on the dynamic font, refer to the [Dynamic Font Scaling doc
- `size` has been removed from the `ion-input` component. Developers should use CSS to specify the visible width of the input.
- `accept` has been removed from the `ion-input` component. This was previously used in conjunction with the `type="file"`. However, the `file` value for `type` is not a valid value in Ionic Framework.
- The `legacy` property and support for the legacy syntax, which involved placing an `ion-input` inside of an `ion-item` with an `ion-label`, have been removed. For more information on migrating from the legacy input syntax, refer to the [Input documentation](https://ionicframework.com/docs/api/input#migrating-from-legacy-input-syntax).
<h4 id="version-8x-nav">Nav</h4>

View File

@@ -566,7 +566,6 @@ ion-input,prop,helperText,string | undefined,undefined,false,false
ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false
ion-input,prop,label,string | undefined,undefined,false,false
ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start",'start',false,false
ion-input,prop,legacy,boolean | undefined,undefined,false,false
ion-input,prop,max,number | string | undefined,undefined,false,false
ion-input,prop,maxlength,number | undefined,undefined,false,false
ion-input,prop,min,number | string | undefined,undefined,false,false

View File

@@ -1220,10 +1220,6 @@ export namespace Components {
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement": 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/
@@ -5951,10 +5947,6 @@ declare namespace LocalJSX {
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the input in LTR and to the right in RTL. `"end"`: The label will appear to the right of the input in LTR and to the left in RTL. `"floating"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `"stacked"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
"labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
"legacy"?: boolean;
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/

View File

@@ -8,21 +8,6 @@
font-size: $input-ios-font-size;
}
// TODO FW-2764 Remove this
:host(.legacy-input) {
--padding-top: #{$input-ios-padding-top};
--padding-end: #{$input-ios-padding-end};
--padding-bottom: #{$input-ios-padding-bottom};
--padding-start: #{$input-ios-padding-start};
}
:host-context(.item-label-stacked),
:host-context(.item-label-floating) {
--padding-top: 8px;
--padding-bottom: 8px;
--padding-start: 0px;
}
.input-clear-icon ion-icon {
width: 18px;
height: 18px;
@@ -33,7 +18,6 @@
// The input, label, helper text, char counter and placeholder
// should use the same opacity and match the other form controls
:host(.legacy-input) .native-input[disabled],
:host(.input-disabled) {
opacity: #{$input-ios-disabled-opacity};
}

View File

@@ -7,17 +7,5 @@
/// @prop - Font size of the input
$input-ios-font-size: inherit !default;
/// @prop - Margin top of the input
$input-ios-padding-top: $item-ios-padding-top !default;
/// @prop - Margin end of the input
$input-ios-padding-end: ($item-ios-padding-end * 0.5) !default;
/// @prop - Margin bottom of the input
$input-ios-padding-bottom: $item-ios-padding-bottom !default;
/// @prop - Margin start of the input
$input-ios-padding-start: 0 !default;
/// @prop - The opacity of the input text, label, helper text, char counter and placeholder of a disabled input
$input-ios-disabled-opacity: $form-control-ios-disabled-opacity !default;

View File

@@ -14,21 +14,6 @@
font-size: $input-md-font-size;
}
// TODO FW-2764 Remove this
:host(.legacy-input) {
--padding-top: #{$input-md-padding-top};
--padding-end: #{$input-md-padding-end};
--padding-bottom: #{$input-md-padding-bottom};
--padding-start: #{$input-md-padding-start};
}
:host-context(.item-label-stacked),
:host-context(.item-label-floating) {
--padding-top: 8px;
--padding-bottom: 8px;
--padding-start: 0;
}
.input-clear-icon ion-icon {
width: 22px;
height: 22px;
@@ -39,7 +24,6 @@
// The input, label, helper text, char counter and placeholder
// should use the same opacity and match the other form controls
:host(.legacy-input) .native-input[disabled],
:host(.input-disabled) {
opacity: #{$input-md-disabled-opacity};
}

View File

@@ -7,18 +7,6 @@
/// @prop - Font size of the input
$input-md-font-size: inherit !default;
/// @prop - Margin top of the input
$input-md-padding-top: 10px !default;
/// @prop - Margin end of the input
$input-md-padding-end: 0 !default;
/// @prop - Margin bottom of the input
$input-md-padding-bottom: 10px !default;
/// @prop - Margin start of the input
$input-md-padding-start: ($item-md-padding-start * 0.5) !default;
/// @prop - The amount of whitespace to display on either side of the floating label
$input-md-floating-label-padding: 4px !default;

View File

@@ -57,6 +57,8 @@
width: 100%;
min-height: 44px;
/* stylelint-disable-next-line all */
padding: 0 !important;
@@ -67,36 +69,11 @@
z-index: $z-index-item-input;
}
// TODO FW-2764 Remove this
:host(.legacy-input) {
display: flex;
flex: 1;
align-items: center;
background: var(--background);
}
// TODO FW-2764 Remove this
:host(.legacy-input) .native-input {
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
@include border-radius(var(--border-radius));
}
:host-context(ion-item:not(.item-label):not(.item-has-modern-input)) {
--padding-start: 0;
}
:host-context(ion-item)[slot="start"],
:host-context(ion-item)[slot="end"] {
width: auto;
}
// TODO FW-2764 Remove this
:host(.legacy-input.ion-color) {
color: current-color(base);
}
:host(.ion-color) {
--highlight-color-focused: #{current-color(base)};
}
@@ -104,10 +81,6 @@
// Input Wrapper
// ----------------------------------------------------------------
:host(:not(.legacy-input)) {
min-height: 44px;
}
/**
* Since the label sits on top of the element,
* the component needs to be taller otherwise the
@@ -206,11 +179,6 @@
// Clear Input Icon
// --------------------------------------------------
// TODO FW-2764 Remove this
:host(.legacy-input) .input-clear-icon {
@include margin(0);
}
.input-clear-icon {
@include margin(auto);
@include padding(0);
@@ -256,37 +224,6 @@
visibility: visible;
}
// Input Has focus
// --------------------------------------------------
// TODO FW-2764 Remove this
:host(.has-focus.legacy-input) {
pointer-events: none;
}
// TODO FW-2764 Remove this
:host(.has-focus.legacy-input) input,
:host(.has-focus.legacy-input) a,
:host(.has-focus.legacy-input) button {
pointer-events: auto;
}
// Item Floating: Placeholder
// ----------------------------------------------------------------
// When used with a floating item the placeholder should hide
:host-context(.item-label-floating.item-has-placeholder:not(.item-has-value)) {
opacity: 0;
}
:host-context(.item-label-floating.item-has-placeholder:not(.item-has-value).item-has-focus) {
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 1;
}
// Input Wrapper
// ----------------------------------------------------------------

View File

@@ -1,16 +1,9 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
import type { LegacyFormController, NotchController } from '@utils/forms';
import { createLegacyFormController, createNotchController } from '@utils/forms';
import type { NotchController } from '@utils/forms';
import { createNotchController } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
import {
inheritAriaAttributes,
debounceEvent,
findItemLabel,
inheritAttributes,
componentOnReady,
} from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
import { createSlotMutationController } from '@utils/slot-mutation-controller';
import type { SlotMutationController } from '@utils/slot-mutation-controller';
import { createColorClasses, hostContext } from '@utils/theme';
@@ -42,13 +35,10 @@ export class Input implements ComponentInterface {
private inputId = `ion-input-${inputIds++}`;
private inheritedAttributes: Attributes = {};
private isComposing = false;
private legacyFormController!: LegacyFormController;
private slotMutationController?: SlotMutationController;
private notchController?: NotchController;
private notchSpacerEl: HTMLElement | undefined;
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
private originalIonInput?: EventEmitter<InputInputEventDetail>;
/**
@@ -142,11 +132,6 @@ export class Input implements ComponentInterface {
*/
@Prop() disabled = false;
@Watch('disabled')
protected disabledChanged() {
this.emitStyle();
}
/**
* A hint to the browser for which enter key to display.
* Possible values: `"enter"`, `"done"`, `"go"`, `"next"`,
@@ -196,17 +181,6 @@ export class Input implements ComponentInterface {
*/
@Prop() labelPlacement: 'start' | 'end' | 'floating' | 'stacked' | 'fixed' = 'start';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup.
* Ionic will only opt components in to the modern form markup when they are
* using either the `aria-label` attribute or the `label` property. As a result,
* the `legacy` property should only be used as an escape hatch when you want to
* avoid this automatic opt-in behavior.
* Note that this property will be removed in an upcoming major release
* of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@Prop() legacy?: boolean;
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/
@@ -327,14 +301,6 @@ export class Input implements ComponentInterface {
*/
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
/**
* Update the item classes when the placeholder changes
*/
@Watch('placeholder')
protected placeholderChanged() {
this.emitStyle();
}
/**
* Update the native input element when the value changes
*/
@@ -353,7 +319,6 @@ export class Input implements ComponentInterface {
*/
nativeInput.value = value;
}
this.emitStyle();
}
componentWillLoad() {
@@ -366,7 +331,6 @@ export class Input implements ComponentInterface {
connectedCallback() {
const { el } = this;
this.legacyFormController = createLegacyFormController(el);
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
this.notchController = createNotchController(
el,
@@ -374,7 +338,6 @@ export class Input implements ComponentInterface {
() => this.labelSlot
);
this.emitStyle();
this.debounceChanged();
if (Build.isBrowser) {
document.dispatchEvent(
@@ -483,21 +446,6 @@ export class Input implements ComponentInterface {
return typeof this.value === 'number' ? this.value.toString() : (this.value || '').toString();
}
private emitStyle() {
if (this.legacyFormController.hasLegacyControl()) {
this.ionStyle.emit({
interactive: true,
input: true,
'has-placeholder': this.placeholder !== undefined,
'has-value': this.hasValue(),
'has-focus': this.hasFocus,
'interactive-disabled': this.disabled,
// TODO(FW-2764): remove this
legacy: !!this.legacy,
});
}
}
private onInput = (ev: InputEvent | Event) => {
const input = ev.target as HTMLInputElement | null;
if (input) {
@@ -512,7 +460,6 @@ export class Input implements ComponentInterface {
private onBlur = (ev: FocusEvent) => {
this.hasFocus = false;
this.emitStyle();
if (this.focusedValue !== this.value) {
/**
@@ -530,7 +477,6 @@ export class Input implements ComponentInterface {
private onFocus = (ev: FocusEvent) => {
this.hasFocus = true;
this.focusedValue = this.value;
this.emitStyle();
this.ionFocus.emit(ev);
};
@@ -718,7 +664,7 @@ export class Input implements ComponentInterface {
return this.renderLabel();
}
private renderInput() {
render() {
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus } = this;
const mode = getIonMode(this);
const value = this.getValue();
@@ -833,112 +779,6 @@ export class Input implements ComponentInterface {
</Host>
);
}
// TODO FW-2764 Remove this
private renderLegacyInput() {
if (!this.hasLoggedDeprecationWarning) {
printIonWarning(
`ion-input now requires providing a label with either the "label" property or the "aria-label" attribute. To migrate, remove any usage of "ion-label" and pass the label text to either the "label" property or the "aria-label" attribute.
Example: <ion-input label="Email"></ion-input>
Example with aria-label: <ion-input aria-label="Email"></ion-input>
For inputs that do not render the label immediately next to the input, developers may continue to use "ion-label" but must manually associate the label with the input by using "aria-labelledby".
Developers can use the "legacy" property to continue using the legacy form markup. This property will be removed in an upcoming major release of Ionic where this form control will use the modern form markup.`,
this.el
);
if (this.legacy) {
printIonWarning(
`ion-input is being used with the "legacy" property enabled which will forcibly enable the legacy form markup. This property will be removed in an upcoming major release of Ionic where this form control will use the modern form markup.
Developers can dismiss this warning by removing their usage of the "legacy" property and using the new input syntax.`,
this.el
);
}
this.hasLoggedDeprecationWarning = true;
}
const mode = getIonMode(this);
const value = this.getValue();
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return (
<Host
aria-disabled={this.disabled ? 'true' : null}
class={createColorClasses(this.color, {
[mode]: true,
'has-value': this.hasValue(),
'has-focus': this.hasFocus,
'legacy-input': true,
'in-item-color': hostContext('ion-item.ion-color', this.el),
})}
>
<input
class="native-input"
ref={(input) => (this.nativeInput = input)}
aria-labelledby={label ? label.id : null}
disabled={this.disabled}
autoCapitalize={this.autocapitalize}
autoComplete={this.autocomplete}
autoCorrect={this.autocorrect}
autoFocus={this.autofocus}
enterKeyHint={this.enterkeyhint}
inputMode={this.inputmode}
min={this.min}
max={this.max}
minLength={this.minlength}
maxLength={this.maxlength}
multiple={this.multiple}
name={this.name}
pattern={this.pattern}
placeholder={this.placeholder || ''}
readOnly={this.readonly}
required={this.required}
spellcheck={this.spellcheck}
step={this.step}
type={this.type}
value={value}
onInput={this.onInput}
onChange={this.onChange}
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyDown={this.onKeydown}
{...this.inheritedAttributes}
/>
{this.clearInput && !this.readonly && !this.disabled && (
<button
aria-label="reset"
type="button"
class="input-clear-icon"
onPointerDown={(ev) => {
/**
* This prevents mobile browsers from
* blurring the input when the clear
* button is activated.
*/
ev.preventDefault();
}}
onClick={this.clearTextInput}
>
<ion-icon aria-hidden="true" icon={mode === 'ios' ? closeCircle : closeSharp}></ion-icon>
</button>
)}
</Host>
);
}
render() {
const { legacyFormController } = this;
return legacyFormController.hasLegacyControl() ? this.renderLegacyInput() : this.renderInput();
}
}
let inputIds = 0;

View File

@@ -1,27 +0,0 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../../item/item';
import { Input } from '../../input';
it('should render as modern when label is set asynchronously', async () => {
const page = await newSpecPage({
components: [Item, Input],
html: `
<ion-item>
<ion-input></ion-input>
</ion-item>
`,
});
const input = page.body.querySelector('ion-input')!;
// Template should be modern
expect(input.classList.contains('legacy-input')).toBe(false);
// Update the input label
input.label = 'New label';
await page.waitForChanges();
// Template should still be modern
expect(input.classList.contains('legacy-input')).toBe(false);
});

View File

@@ -1,35 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('input: a11y'), () => {
test('does not set a default aria-labelledby when there is not a neighboring ion-label', async ({ page }) => {
await page.setContent('<ion-input legacy="true"></ion-input>', config);
const input = page.locator('ion-input > input');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
await expect(ariaLabelledBy).toBe(null);
});
test('set a default aria-labelledby when a neighboring ion-label exists', async ({ page }) => {
await page.setContent(
`
<ion-item>
<ion-label>A11y Test</ion-label>
<ion-input></ion-input>
</ion-item>
`,
config
);
const label = page.locator('ion-label');
const input = page.locator('ion-input > input');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
const labelId = await label.getAttribute('id');
await expect(ariaLabelledBy).toBe(labelId);
});
});
});

View File

@@ -1,186 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - Attributes</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>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - Attributes</ion-title>
<ion-buttons slot="primary">
<ion-button>
<ion-icon slot="icon-only" name="menu"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-label id="itemless">My Label</ion-label>
<ion-input aria-labelledby="itemless"></ion-input>
<ion-item>
<ion-label position="stacked">Number</ion-label>
<ion-input
id="input1"
type="number"
placeholder="Placeholder"
value="1234"
name="holaa"
min="0"
max="10000"
step="2"
autocomplete="on"
autocorrect="on"
autocapitalize="on"
spellcheck="true"
maxlength="4"
disabled
readonly
></ion-input>
</ion-item>
<ion-item id="custom">
<ion-label position="stacked">Text</ion-label>
<ion-input id="input2" type="text" placeholder="my placeholder" value="test"></ion-input>
</ion-item>
<ion-item>
<ion-label id="myLabel" stacked>Default</ion-label>
<ion-input id="input3" value="inputs"></ion-input>
</ion-item>
<ion-item>
<ion-input id="input4" placeholder="No Label" aria-label="input4"></ion-input>
</ion-item>
<ion-list>
<ion-item>
Number Test&nbsp;
<span id="numberInputResult"></span>
</ion-item>
<ion-item>
Text Test&nbsp;
<span id="textInputResult"></span>
</ion-item>
<ion-item>
Default Test&nbsp;
<span id="defaultInputResult"></span>
</ion-item>
<ion-item>
No Label Test&nbsp;
<span id="noLabelInputResult"></span>
</ion-item>
</ion-list>
</ion-list>
</ion-content>
<script>
var numberInput = checkInput('input1');
updateResult(numberInput, 'numberInputResult');
var textInput = checkInput('input2');
updateResult(textInput, 'textInputResult');
var defaultInput = checkInput('input3');
updateResult(defaultInput, 'defaultInputResult');
var noLabelInput = checkInput('input4');
updateResult(noLabelInput, 'noLabelInputResult');
// Update results of input
function updateResult(result, resultId) {
var resultEl = document.getElementById(resultId);
resultEl.innerHTML = result ? 'passed' : 'FAILED';
var itemEl = resultEl.closest('ion-item');
itemEl.color = result ? 'secondary' : 'danger';
}
function checkInput(id) {
var el = document.getElementById(id);
var inputEl = el.querySelector('input');
if (id === 'input1') {
return testAttributes(el, inputEl, {
id: 'input1',
type: 'number',
placeholder: 'Placeholder',
name: 'holaa',
min: '0',
max: '10000',
step: '2',
autocomplete: 'on',
autocorrect: 'on',
autocapitalize: 'on',
spellcheck: 'true',
maxLength: '4',
'aria-labelledby': 'lbl-0',
readonly: true,
disabled: true,
});
} else if (id === 'input2') {
return testAttributes(el, inputEl, {
id: 'input2',
type: 'text',
placeholder: 'my placeholder',
value: 'test',
readonly: undefined,
disabled: undefined,
});
} else if (id === 'input3') {
return testAttributes(el, inputEl, {
id: 'input3',
type: undefined,
value: 'inputs',
readonly: undefined,
disabled: undefined,
});
} else if (id === 'input4') {
return testAttributes(el, inputEl, {
id: 'input4',
type: undefined,
readonly: undefined,
disabled: undefined,
'aria-label': 'input4',
});
}
return false;
}
function testAttributes(el, inputEl, attributes) {
for (let attr in attributes) {
const expected = attributes[attr];
const value = el[attr];
if (expected === null) {
if (el.hasAttribute(attr) || value !== '') {
console.error(`Element should NOT have "${attr}"`);
return false;
}
} else {
if (expected !== value && expected !== el.getAttribute(attr)) {
console.error(`Value "${attr}" does not match: ${expected} != ${value}`);
return false;
}
}
}
return true;
}
</script>
</ion-app>
</body>
</html>

View File

@@ -1,169 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - Basic</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>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content id="content">
<ion-list>
<ion-item>
<ion-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
</ion-item>
<ion-item>
<ion-input placeholder="Placeholder"></ion-input>
</ion-item>
<ion-item lines="full" id="fullItem">
<ion-input id="fullInput" placeholder="Full"></ion-input>
</ion-item>
<ion-item lines="inset" id="insetItem">
<ion-input id="insetInput" placeholder="Inset"></ion-input>
</ion-item>
<ion-item lines="none" id="noneItem">
<ion-input id="noneInput" placeholder="None"></ion-input>
</ion-item>
<ion-item>
<ion-label>Default Label</ion-label>
<ion-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
</ion-item>
<ion-item>
<ion-label>Clear Input</ion-label>
<ion-input
clear-input
value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"
></ion-input>
</ion-item>
<ion-item color="dark">
<ion-label position="floating">Floating</ion-label>
<ion-input checked></ion-input>
</ion-item>
<ion-item>
<ion-label position="fixed">Type #</ion-label>
<ion-input type="number" value="333"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Password</ion-label>
<ion-input type="password"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Placeholder</ion-label>
<ion-input placeholder="Enter Something"></ion-input>
</ion-item>
<ion-item>
<ion-label>Disabled</ion-label>
<ion-input id="dynamicDisabled" value="Disabled" disabled></ion-input>
</ion-item>
<ion-item>
<ion-label>Readonly</ion-label>
<ion-input id="dynamicReadonly" value="Readonly" readonly></ion-input>
</ion-item>
<ion-item>
<ion-label>Slot</ion-label>
<ion-input slot="start" value="Start"></ion-input>
</ion-item>
<ion-item>
<ion-label>Toggle</ion-label>
<ion-toggle checked slot="end"></ion-toggle>
</ion-item>
<ion-item>
<ion-label position="fixed">Type #</ion-label>
<div type="number" value="333" class="input input-md hydrated">
<!---->
<input
aria-disabled="false"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
autofocus="false"
class="native-input native-input-md"
spellcheck="false"
type="number"
/>
<button type="button" class="input-clear-icon" hidden=""></button>
</div>
</ion-item>
</ion-list>
<div class="ion-text-center">
<ion-button onclick="toggleBoolean('dynamicDisabled', 'disabled')"> Toggle Disabled </ion-button>
<ion-button color="secondary" onclick="toggleBoolean('dynamicReadonly', 'readonly')">
Toggle Readonly
</ion-button>
</div>
<ion-item>
<ion-label>Clear Input</ion-label>
<ion-input
clear-input
value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"
></ion-input>
</ion-item>
<ion-item>
<ion-label>Clear On Edit</ion-label>
<ion-input
clear-on-edit
value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"
></ion-input>
</ion-item>
<ion-item style="max-width: 250px">
<ion-input value="Narrow input"></ion-input>
<ion-label class="ion-text-right">Left</ion-label>
</ion-item>
<ion-item style="max-width: 250px">
<ion-label>Right</ion-label>
<ion-input class="ion-text-right" value="Narrow input"></ion-input>
</ion-item>
</ion-content>
<script>
document.querySelector('ion-input').addEventListener('ionBlur', (ev) => {
console.log(ev);
});
function toggleBoolean(id, prop) {
var el = document.getElementById(id);
var isTrue = el[prop] ? false : true;
el[prop] = isTrue;
console.log('in toggleBoolean, setting', prop, 'to', isTrue);
}
</script>
</ion-app>
</body>
</html>

View File

@@ -1,229 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs().forEach(({ title, screenshot, config }) => {
test.describe(title('input: basic'), () => {
test.describe('input with overflow', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item>
<ion-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges" legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
// Validates the display of an input where text extends off the edge of the component.
await expect(item).toHaveScreenshot(screenshot(`input-with-text-overflow`));
});
});
test.describe('input with placeholder', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item>
<ion-input placeholder="Placeholder" legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
// Validates the display of an input with a placeholder.
await expect(item).toHaveScreenshot(screenshot(`input-with-placeholder`));
});
});
test.describe('input disabled', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item>
<ion-input value="Input disabled" disabled legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
// Validates the display of an input in a disabled state.
await expect(item).toHaveScreenshot(screenshot(`input-disabled`));
});
});
test.describe('input with lines="full"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item lines="full">
<ion-input placeholder="Full" legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="full".
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-full`));
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="full" when focused.
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-full-focused`));
});
});
test.describe('input with lines="inset"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item lines="inset">
<ion-input placeholder="Inset" legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="inset".
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-inset`));
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="inset" when focused.
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-inset-focused`));
});
});
test.describe('input with lines="none"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item lines="none">
<ion-input placeholder="None" legacy="true"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="none".
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-none`));
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="none" when focused.
await expect(item).toHaveScreenshot(screenshot(`input-with-lines-none-focused`));
});
});
test.describe('input with clear button', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<ion-content>
<ion-list>
<ion-item>
<ion-label>Clear Input</ion-label>
<ion-input
clear-input
value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"
legacy="true"
></ion-input>
</ion-item>
</ion-list>
</ion-content>
`,
config
);
const item = page.locator('ion-item');
// Validates the display of an input with a clear button.
await expect(item).toHaveScreenshot(screenshot(`input-with-clear-button`));
});
});
});
});
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('input: clear button'), () => {
test('should clear the input when pressed', async ({ page }) => {
await page.setContent(
`
<ion-input value="abc" clear-input="true" legacy="true"></ion-input>
`,
config
);
const input = page.locator('ion-input');
const clearButton = input.locator('.input-clear-icon');
await expect(input).toHaveJSProperty('value', 'abc');
await clearButton.click();
await page.waitForChanges();
await expect(input).toHaveJSProperty('value', '');
});
/**
* Note: This only tests the desktop focus behavior.
* Mobile browsers have different restrictions around
* focusing inputs, so these platforms should always
* be tested when making changes to the focus behavior.
*/
test('should keep the input focused when the clear button is pressed', async ({ page }) => {
await page.setContent(
`
<ion-input value="abc" clear-input="true" legacy="true"></ion-input>
`,
config
);
const input = page.locator('ion-input');
const nativeInput = input.locator('input');
const clearButton = input.locator('.input-clear-icon');
await input.click();
await expect(nativeInput).toBeFocused();
await clearButton.click();
await page.waitForChanges();
await expect(nativeInput).toBeFocused();
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Some files were not shown because too many files have changed in this diff Show More