Compare commits

...

38 Commits

Author SHA1 Message Date
Brandy Smith
98044434a1 break radio group for dev build 2025-03-05 12:50:03 -05:00
Brandy Smith
a53ee64940 style: add FW issue for tracking Stencil issue 2025-03-05 10:03:07 -05:00
Brandy Smith
d4230a0db5 Merge branch 'feature-8.5' into ROU-11554 2025-03-05 09:22:59 -05:00
Brandy Smith
eb427931bd test(radio-group): update e2e test to verify correct element 2025-03-05 09:22:54 -05:00
Brandy Smith
4322935540 fix(textarea): update helper text and counter color (#30148)
Issue number: N/A

---------

## What is the current behavior?
Helper text is lighter than it should be.

## What is the new behavior?
- Updates helper and counter text to match MD design
- Updates e2e test to include more coverage

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information


[Preview](https://ionic-framework-git-rou-11559-ionic1.vercel.app/src/components/textarea/test/bottom-content)

> Note that the fill toggle will only work in `md` mode

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-03-04 16:00:00 -05:00
Brandy Smith
fdd52832c6 fix(input): update helper text and counter color (#30149)
Issue number: N/A

---------

## What is the current behavior?
Helper text is lighter than it should be.

## What is the new behavior?
- Updates helper and counter text to match MD design
- Updates e2e test to include more coverage

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information


[Preview](https://ionic-framework-git-rou-11558-ionic1.vercel.app/src/components/input/test/bottom-content)

> Note that the fill toggle will only work in `md` mode

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-03-04 15:29:35 -05:00
Brandy Smith
c222328a10 Merge branch 'feature-8.5' into ROU-11554 2025-03-04 11:24:55 -05:00
Brandy Smith
11f607a8a2 style: comment 2025-03-04 10:42:35 -05:00
Brandy Smith
051c74bacf test(radio): update css to max 3 per row but wrap 2025-03-04 10:38:17 -05:00
Brandy Smith
55e1c294a5 fix(radio-group): use inline div to avoid extra spacing 2025-03-04 10:24:32 -05:00
Brandy Smith
33417fe551 chore(): add updated snapshots 2025-03-04 10:23:46 -05:00
Brandy Smith
5c8a1abbdf test(radio-group): fix custom css 2025-03-04 10:17:02 -05:00
Brandy Smith
1e925bab45 fix(radio-group): use proper classes and colors 2025-03-04 09:55:15 -05:00
Brandy Smith
22d59cfe41 style: code comments 2025-03-04 09:40:44 -05:00
Brandy Smith
5edfdcf14c refactor(radio-group): change supporting-text class to radio-group-top 2025-03-04 09:39:02 -05:00
Brandy Smith
15c8a69fcb fix(radio-group): wrap slot in a div to resolve blurring on focus 2025-03-04 09:33:55 -05:00
Shane
f4941f2639 feat(react): fixing support for react 19, adding test app for react 19 (#30217)
Issue number: resolves #29991

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-03-03 08:50:05 -08:00
Brandy Smith
521d077376 chore(git): update 8.5 to be in sync with main (#30224) 2025-03-03 09:45:34 -05:00
ShaneK
ecaeb390cf Merge branch 'main' of github.com:ionic-team/ionic-framework into merge/main-into-8.5 2025-03-03 05:44:37 -08:00
Brandy Smith
d71d7f49d1 style: lint 2025-02-28 16:16:00 -05:00
Brandy Smith
b9d8cb9925 fix(angular): mark radio group as touched when radio is blurred 2025-02-28 16:01:00 -05:00
Brandy Smith
ef31de3fa8 fix(radio-group): fix firefox alignment 2025-02-27 17:29:45 -05:00
Brandy Smith
5c234570b7 chore(): add updated snapshots 2025-02-27 16:37:23 -05:00
Brandy Smith
e3795e384b fix(radio-group): remove display 2025-02-27 16:35:14 -05:00
Brandy Smith
fbf4f849ba chore(): add updated snapshots 2025-02-27 16:07:42 -05:00
Brandy Smith
f20d8ed920 fix(radio-group): use inline block to avoid taking up entire width 2025-02-27 16:07:25 -05:00
Brandy Smith
c1ac2bfde9 chore: build 2025-02-27 14:39:18 -05:00
Brandy Smith
b9452776a3 chore(): add updated snapshots 2025-02-27 14:21:38 -05:00
Brandy Smith
a8d5165675 test(radio-group): add e2e test for supporting text 2025-02-27 14:21:04 -05:00
Brandy Smith
0827a4cffe feat(radio-group): add helperText and errorText properties 2025-02-27 14:20:59 -05:00
Giuliana Silva
41da4c3565 feat(checkbox): add required prop (#30157)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `required` attribute to checkbox's inner native input.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->
2025-02-04 11:54:23 +00:00
Giuliana Silva
0b549835b6 feat(select): add required prop (#30155)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `aria-required` attribute to select's inner native button.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2025-02-04 10:18:40 +00:00
Giuliana Silva
0bbb9f37b4 feat(toggle): add required prop (#30156)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `required` attribute to toggle's inner native input.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->
2025-02-04 09:59:01 +00:00
Israel de la Barrera
166e43554e feat(modal): add expandToScroll property to allow scrolling at all breakpoints (#30097)
Issue number: resolves #24631

Co-authored-by: Maria Hutt <13530427+thetaPC@users.noreply.github.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
2025-02-03 10:45:15 -08:00
Brandy Smith
621333d927 chore(git): update feature-8.5 from main (#30168) 2025-01-29 14:34:11 -05:00
Brandy Smith
6cf454f7c4 Merge branch 'main' into chore-main-to-8.5 2025-01-29 14:20:00 -05:00
fudom
ac4ea3232b feat(toolbar): add shadow parts for background, container, and content (#30069)
Resolves #30068

---------

Add `part` attributes to toolbar for the `background`, `container` and `content`.
2025-01-23 15:48:58 -05:00
Sean Perkins
efd3e0fd2b feat(config): add logLevel option to suppress ionic warnings and errors (#30015)
resolves #29814

---------

- Developers can assign a value to `logLevel` in the Ionic config to control the log level that Ionic Framework will produce logs for.
  - `OFF` will completely disable all warnings and errors from Ionic
  - `WARN` will log warnings and errors
  - `ERROR` will log only errors
- Default behavior is that developers receive both Ionic warnings and errors
- Configuration only applies to usages of `printIonWarning` and `printIonError`

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-01-20 16:15:02 -05:00
608 changed files with 12948 additions and 717 deletions

View File

@@ -198,7 +198,7 @@ jobs:
strategy:
fail-fast: false
matrix:
apps: [react17, react18]
apps: [react17, react18, react19]
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:

View File

@@ -208,7 +208,7 @@ jobs:
strategy:
fail-fast: false
matrix:
apps: [react17, react18]
apps: [react17, react18, react19]
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:

View File

@@ -403,6 +403,7 @@ ion-checkbox,prop,justify,"end" | "space-between" | "start" | undefined,undefine
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
ion-checkbox,prop,name,string,this.inputId,false,false
ion-checkbox,prop,required,boolean,false,false,false
ion-checkbox,prop,value,any,'on',false,false
ion-checkbox,event,ionBlur,void,true
ion-checkbox,event,ionChange,CheckboxChangeEventDetail<any>,true
@@ -1074,6 +1075,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise<boolean>) | boolean,true,false,false
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,expandToScroll,boolean,true,false,false
ion-modal,prop,focusTrap,boolean,true,false,false
ion-modal,prop,handle,boolean | undefined,undefined,false,false
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
@@ -1345,6 +1347,8 @@ ion-radio,part,mark
ion-radio-group,none
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
ion-radio-group,prop,errorText,string | undefined,undefined,false,false
ion-radio-group,prop,helperText,string | undefined,undefined,false,false
ion-radio-group,prop,name,string,this.inputId,false,false
ion-radio-group,prop,value,any,undefined,false,false
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
@@ -1631,6 +1635,7 @@ ion-select,prop,multiple,boolean,false,false,false
ion-select,prop,name,string,this.inputId,false,false
ion-select,prop,okText,string,'OK',false,false
ion-select,prop,placeholder,string | undefined,undefined,false,false
ion-select,prop,required,boolean,false,false,false
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
ion-select,prop,shape,"round" | undefined,undefined,false,false
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
@@ -1944,6 +1949,7 @@ ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
ion-toggle,prop,name,string,this.inputId,false,false
ion-toggle,prop,required,boolean,false,false,false
ion-toggle,prop,value,null | string | undefined,'on',false,false
ion-toggle,event,ionBlur,void,true
ion-toggle,event,ionChange,ToggleChangeEventDetail<any>,true
@@ -2000,4 +2006,7 @@ ion-toolbar,css-prop,--padding-end,md
ion-toolbar,css-prop,--padding-start,ios
ion-toolbar,css-prop,--padding-start,md
ion-toolbar,css-prop,--padding-top,ios
ion-toolbar,css-prop,--padding-top,md
ion-toolbar,css-prop,--padding-top,md
ion-toolbar,part,background
ion-toolbar,part,container
ion-toolbar,part,content

View File

@@ -643,6 +643,10 @@ export namespace Components {
* The name of the control, which is submitted with the form data.
*/
"name": string;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required": boolean;
"setFocus": () => Promise<void>;
/**
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
@@ -1731,6 +1735,10 @@ export namespace Components {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle.
*/
"expandToScroll": boolean;
/**
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
*/
@@ -2299,6 +2307,14 @@ export namespace Components {
* This property allows developers to specify a custom function or property name for comparing objects when determining the selected option in the ion-radio-group. When not specified, the default behavior will use strict equality (===) for comparison.
*/
"compareWith"?: string | RadioGroupCompareFn | null;
/**
* The error text to display at the top of the radio group.
*/
"errorText"?: string;
/**
* The helper text to display at the top of the radio group.
*/
"helperText"?: string;
/**
* The name of the control, which is submitted with the form data.
*/
@@ -2808,6 +2824,10 @@ export namespace Components {
* The text to display when the select is empty.
*/
"placeholder"?: string;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required": boolean;
/**
* The text to display instead of the selected option's value.
*/
@@ -3280,6 +3300,10 @@ export namespace Components {
* The name of the control, which is submitted with the form data.
*/
"name": string;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required": boolean;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
*/
@@ -5435,6 +5459,10 @@ declare namespace LocalJSX {
* Emitted when the checkbox has focus.
*/
"onIonFocus"?: (event: IonCheckboxCustomEvent<void>) => void;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required"?: boolean;
/**
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
*/
@@ -6532,6 +6560,10 @@ declare namespace LocalJSX {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle.
*/
"expandToScroll"?: boolean;
/**
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
*/
@@ -7055,6 +7087,14 @@ declare namespace LocalJSX {
* This property allows developers to specify a custom function or property name for comparing objects when determining the selected option in the ion-radio-group. When not specified, the default behavior will use strict equality (===) for comparison.
*/
"compareWith"?: string | RadioGroupCompareFn | null;
/**
* The error text to display at the top of the radio group.
*/
"errorText"?: string;
/**
* The helper text to display at the top of the radio group.
*/
"helperText"?: string;
/**
* The name of the control, which is submitted with the form data.
*/
@@ -7640,6 +7680,10 @@ declare namespace LocalJSX {
* The text to display when the select is empty.
*/
"placeholder"?: string;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required"?: boolean;
/**
* The text to display instead of the selected option's value.
*/
@@ -8155,6 +8199,10 @@ declare namespace LocalJSX {
* Emitted when the toggle has focus.
*/
"onIonFocus"?: (event: IonToggleCustomEvent<void>) => void;
/**
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
*/
"required"?: boolean;
/**
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
*/

View File

@@ -98,6 +98,13 @@ export class Checkbox implements ComponentInterface {
*/
@Prop() alignment?: 'start' | 'center';
/**
* If true, screen readers will announce it as a required field. This property
* works only for accessibility purposes, it will not prevent the form from
* submitting if the value is invalid.
*/
@Prop() required = false;
/**
* Emitted when the checked property has changed as a result of a user action such as a click.
*
@@ -182,6 +189,7 @@ export class Checkbox implements ComponentInterface {
name,
value,
alignment,
required,
} = this;
const mode = getIonMode(this);
const path = getSVGPath(mode, indeterminate);
@@ -218,6 +226,7 @@ export class Checkbox implements ComponentInterface {
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
required={required}
{...inheritedAttributes}
/>
<div

View File

@@ -54,3 +54,33 @@ describe('ion-checkbox: indeterminate', () => {
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
});
});
describe('ion-checkbox: required', () => {
it('should have a required attribute in inner input when true', async () => {
const page = await newSpecPage({
components: [Checkbox],
html: `
<ion-checkbox required="true">Checkbox</ion-checkbox>
`,
});
const checkbox = page.body.querySelector('ion-checkbox')!;
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
expect(nativeInput.hasAttribute('required')).toBeTruthy();
});
it('should not have a required attribute in inner input when false', async () => {
const page = await newSpecPage({
components: [Checkbox],
html: `
<ion-checkbox required="false">Checkbox</ion-checkbox>
`,
});
const checkbox = page.body.querySelector('ion-checkbox')!;
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
expect(nativeInput.hasAttribute('required')).toBeFalsy();
});
});

View File

@@ -32,6 +32,10 @@
--border-color: var(--highlight-color);
}
/**
* The bottom content should never have
* a border with the solid style.
*/
:host(.input-fill-solid) .input-bottom {
border-top: none;
}

View File

@@ -306,6 +306,8 @@
border-top: var(--border-width) var(--border-style) var(--border-color);
font-size: dynamic-font(12px);
white-space: normal;
}
/**
@@ -340,7 +342,7 @@
.input-bottom .helper-text {
display: block;
color: #{$text-color-step-450};
color: $text-color-step-300;
}
:host(.ion-touched.ion-invalid) .input-bottom .error-text {
@@ -362,7 +364,7 @@
*/
@include margin-horizontal(auto, null);
color: #{$text-color-step-450};
color: $text-color-step-300;
white-space: nowrap;

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -15,10 +15,11 @@
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
@@ -27,12 +28,6 @@
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;
@@ -40,7 +35,7 @@
</style>
</head>
<body>
<body onLoad="onLoad()">
<ion-app>
<ion-header>
<ion-toolbar>
@@ -52,67 +47,120 @@
<div class="grid">
<div class="grid-item">
<h2>No Hint</h2>
<ion-input label="Email"></ion-input>
<ion-input label="Label"></ion-input>
</div>
<div class="grid-item">
<h2>Helper Hint</h2>
<ion-input label="Email" helper-text="Enter your email"></ion-input>
<h2>No Hint: Stacked</h2>
<ion-input label="Label" label-placement="stacked"></ion-input>
</div>
<div class="grid-item">
<h2>Error Hint</h2>
<h2>Helper Text</h2>
<ion-input label="Label" helper-text="Helper text"></ion-input>
</div>
<div class="grid-item">
<h2>Helper Text: Stacked</h2>
<ion-input label="Label" label-placement="stacked" helper-text="Helper text"></ion-input>
</div>
<div class="grid-item">
<h2>Error Text</h2>
<ion-input class="ion-touched ion-invalid" label="Label" error-text="Error text"></ion-input>
</div>
<div class="grid-item">
<h2>Error Text: Stacked</h2>
<ion-input
class="ion-touched ion-invalid"
label="Email"
error-text="Please enter a valid email"
label="Label"
label-placement="stacked"
error-text="Error text"
></ion-input>
</div>
<div class="grid-item">
<h2>Custom Error Color</h2>
<h2>Error Text: Custom Color</h2>
<ion-input
class="ion-touched ion-invalid custom-error-color"
label="Email"
error-text="Please enter a valid email"
label="Label"
error-text="Error text"
></ion-input>
</div>
<div class="grid-item">
<h2>Helper Text: Wrapping</h2>
<ion-input
label="Label"
helper-text="Helper text helper text helper text helper text helper text helper text helper text helper text helper text"
>
</ion-input>
</div>
<div class="grid-item">
<h2>Counter</h2>
<ion-input label="Email" counter="true" maxlength="100"></ion-input>
<ion-input label="Label" counter="true" maxlength="100"></ion-input>
</div>
<div class="grid-item">
<h2>Custom Counter</h2>
<ion-input id="custom-counter" label="Email" counter="true" maxlength="100"></ion-input>
<h2>Counter: Custom</h2>
<ion-input id="custom-counter" label="Label" counter="true" maxlength="100"></ion-input>
</div>
<div class="grid-item">
<h2>Counter with Helper</h2>
<ion-input label="Email" counter="true" maxlength="100" helper-text="Enter an email"></ion-input>
<h2>Counter: with Helper</h2>
<ion-input label="Label" counter="true" maxlength="100" helper-text="Helper text"></ion-input>
</div>
<div class="grid-item">
<h2>Counter with Error</h2>
<h2>Counter: with Error</h2>
<ion-input
class="ion-touched ion-invalid"
label="Email"
label="Label"
counter="true"
maxlength="100"
error-text="Please enter a valid email"
error-text="Error text"
></ion-input>
</div>
</div>
<script>
const customCounterInput = document.querySelector('ion-input#custom-counter');
customCounterInput.counterFormatter = (inputLength, maxLength) => {
const length = maxLength - inputLength;
return `${maxLength - inputLength} characters left`;
};
</script>
<button class="expand" onclick="toggleFill()">Toggle Fill</button>
</ion-content>
</ion-app>
<script>
// Hide the toggle fill button on ios mode since it's not supported
function onLoad() {
const toggleFillButton = document.querySelector('button');
if (Ionic.mode === 'ios' && toggleFillButton) {
toggleFillButton.style.display = 'none';
}
}
const customCounterInput = document.querySelector('ion-input#custom-counter');
customCounterInput.counterFormatter = (inputLength, maxLength) => {
const length = maxLength - inputLength;
return `${maxLength - inputLength} characters left`;
};
const inputs = document.querySelectorAll('ion-input');
function toggleFill() {
inputs.forEach((input) => {
switch (input.fill) {
case 'outline':
input.fill = 'solid';
break;
case 'solid':
input.fill = undefined;
break;
default:
input.fill = 'outline';
}
});
}
</script>
</body>
</html>

View File

@@ -1,189 +1,218 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input: bottom content'), () => {
test('entire input component should render correctly with no fill', async ({ page }) => {
await page.setContent(
`
<ion-input value="hi@ionic.io" label="Email" helper-text="Enter an email" maxlength="20" counter="true"></ion-input>
`,
config
);
const input = page.locator('ion-input');
await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-no-fill`));
});
test('entire input component should render correctly with solid fill', async ({ page }) => {
await page.setContent(
`
<ion-input fill="solid" value="hi@ionic.io" label="Email" helper-text="Enter an email" maxlength="20" counter="true"></ion-input>
`,
config
);
const input = page.locator('ion-input');
await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-solid`));
});
test('entire input component should render correctly with outline fill', async ({ page }) => {
await page.setContent(
`
<ion-input fill="outline" value="hi@ionic.io" label="Email" helper-text="Enter an email" maxlength="20" counter="true"></ion-input>
`,
config
);
const input = page.locator('ion-input');
await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-outline`));
});
});
});
/**
* Rendering is the same across modes
* Functionality is the same across modes & directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('input: bottom content functionality'), () => {
test('should not render bottom content if no hint or counter is enabled', async ({ page }) => {
await page.setContent(`<ion-input label="my input"></ion-input>`, config);
test('should not render bottom content if no hint is enabled', async ({ page }) => {
await page.setContent(`<ion-input label="Label"></ion-input>`, config);
const bottomEl = page.locator('ion-input .input-bottom');
await expect(bottomEl).toHaveCount(0);
});
test('helper text should be visible initially', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const helperText = page.locator('ion-input .helper-text');
const errorText = page.locator('ion-input .error-text');
await expect(helperText).toBeVisible();
await expect(helperText).toHaveText('Helper text');
await expect(errorText).toBeHidden();
});
test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const input = page.locator('ion-input input');
const helperText = page.locator('ion-input .helper-text');
const helperTextId = await helperText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(helperTextId);
});
test('error text should be visible when input is invalid', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const helperText = page.locator('ion-input .helper-text');
const errorText = page.locator('ion-input .error-text');
await expect(helperText).toBeHidden();
await expect(errorText).toBeVisible();
await expect(errorText).toHaveText('Error text');
});
test('input should have an aria-describedby attribute when error text is present', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const input = page.locator('ion-input input');
const errorText = page.locator('ion-input .error-text');
const errorTextId = await errorText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(errorTextId);
});
test('input should have aria-invalid attribute when input is invalid', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const input = page.locator('ion-input input');
await expect(input).toHaveAttribute('aria-invalid');
});
test('input should not have aria-invalid attribute when input is valid', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" helper-text="Helper text" error-text="Error text"></ion-input>`,
config
);
const input = page.locator('ion-input input');
await expect(input).not.toHaveAttribute('aria-invalid');
});
test('input should not have aria-describedby attribute when no hint or error text is present', async ({ page }) => {
await page.setContent(`<ion-input label="Label"></ion-input>`, config);
const input = page.locator('ion-input input');
await expect(input).not.toHaveAttribute('aria-describedby');
});
});
});
/**
* Rendering is the same across modes
* Rendering is different across modes
*/
configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input: helper text rendering'), () => {
test('should not have visual regressions when rendering helper text', async ({ page }) => {
await page.setContent(`<ion-input label="Label" helper-text="Helper text"></ion-input>`, config);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-helper-text`));
});
test('should not have visual regressions when rendering helper text with wrapping text', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" helper-text="Helper text helper text helper text helper text helper text helper text helper text helper text helper text"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-helper-text-wrapping`));
});
test('should not have visual regressions when rendering helper text with a stacked label', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" label-placement="stacked" helper-text="Helper text"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-helper-text-stacked-label`));
});
});
test.describe(title('input: error text rendering'), () => {
test('should not have visual regressions when rendering error text', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" class="ion-invalid ion-touched" error-text="Error text"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-error-text`));
});
test('should not have visual regressions when rendering error text with a stacked label', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" class="ion-invalid ion-touched" error-text="Error text" label-placement="stacked"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-error-text-stacked-label`));
});
});
});
/**
* Customizing supporting text is the same across modes and directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input: hint text'), () => {
test.describe('input: hint text functionality', () => {
test('helper text should be visible initially', async ({ page }) => {
await page.setContent(
`<ion-input helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
test.describe(title('input: supporting text customization'), () => {
test('should not have visual regressions when rendering helper text with custom css', async ({ page }) => {
await page.setContent(
`
<style>
ion-input.custom-input.md .input-bottom .helper-text {
font-size: 20px;
color: green;
}
</style>
<ion-input class="custom-input" label="Label" helper-text="Helper text"></ion-input>
`,
config
);
const helperText = page.locator('ion-input .helper-text');
const errorText = page.locator('ion-input .error-text');
await expect(helperText).toBeVisible();
await expect(helperText).toHaveText('my helper');
await expect(errorText).toBeHidden();
});
test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => {
await page.setContent(
`<ion-input helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
const input = page.locator('ion-input input');
const helperText = page.locator('ion-input .helper-text');
const helperTextId = await helperText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(helperTextId);
});
test('error text should be visible when input is invalid', async ({ page }) => {
await page.setContent(
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
const helperText = page.locator('ion-input .helper-text');
const errorText = page.locator('ion-input .error-text');
await expect(helperText).toBeHidden();
await expect(errorText).toBeVisible();
await expect(errorText).toHaveText('my error');
});
test('error text should change when variable is customized', async ({ page }) => {
await page.setContent(
`
<style>
ion-input.custom-input {
--highlight-color-invalid: purple;
}
</style>
<ion-input class="ion-invalid ion-touched custom-input" label="my label" error-text="my error"></ion-input>
`,
config
);
const errorText = page.locator('ion-input .error-text');
await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`));
});
test('input should have an aria-describedby attribute when error text is present', async ({ page }) => {
await page.setContent(
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
const input = page.locator('ion-input input');
const errorText = page.locator('ion-input .error-text');
const errorTextId = await errorText.getAttribute('id');
const ariaDescribedBy = await input.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(errorTextId);
});
test('input should have aria-invalid attribute when input is invalid', async ({ page }) => {
await page.setContent(
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
const input = page.locator('ion-input input');
await expect(input).toHaveAttribute('aria-invalid');
});
test('input should not have aria-invalid attribute when input is valid', async ({ page }) => {
await page.setContent(
`<ion-input helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
config
);
const input = page.locator('ion-input input');
await expect(input).not.toHaveAttribute('aria-invalid');
});
test('input should not have aria-describedby attribute when no hint or error text is present', async ({
page,
}) => {
await page.setContent(`<ion-input label="my input"></ion-input>`, config);
const input = page.locator('ion-input input');
await expect(input).not.toHaveAttribute('aria-describedby');
});
const helperText = page.locator('ion-input');
await expect(helperText).toHaveScreenshot(screenshot(`input-helper-text-custom-css`));
});
test.describe('input: hint text rendering', () => {
test.describe('regular inputs', () => {
test('should not have visual regressions when rendering helper text', async ({ page }) => {
await page.setContent(`<ion-input helper-text="my helper" label="my input"></ion-input>`, config);
test('should not have visual regressions when rendering error text with custom css', async ({ page }) => {
await page.setContent(
`
<style>
ion-input.custom-input.md .input-bottom .error-text {
font-size: 20px;
color: purple;
}
</style>
<ion-input class="ion-invalid ion-touched custom-input" label="Label" error-text="Error text"></ion-input>
`,
config
);
const bottomEl = page.locator('ion-input .input-bottom');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-helper`));
});
test('should not have visual regressions when rendering error text', async ({ page }) => {
await page.setContent(
`<ion-input class="ion-invalid ion-touched" error-text="my helper" label="my input"></ion-input>`,
config
);
const errorText = page.locator('ion-input');
await expect(errorText).toHaveScreenshot(screenshot(`input-error-text-custom-css`));
});
test('should not have visual regressions when rendering error text with a custom css variable', async ({
page,
}) => {
await page.setContent(
`
<style>
ion-input.custom-input {
--highlight-color-invalid: purple;
}
</style>
<ion-input class="ion-invalid ion-touched custom-input" label="Label" error-text="Error text"></ion-input>
`,
config
);
const bottomEl = page.locator('ion-input .input-bottom');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-error`));
});
});
const errorText = page.locator('ion-input');
await expect(errorText).toHaveScreenshot(screenshot(`input-error-text-custom-css-var`));
});
});
});
/**
* Rendering is the same across modes
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input: counter'), () => {
test.describe('input: counter functionality', () => {
test('should not activate if maxlength is not specified even if bottom content is visible', async ({ page }) => {
await page.setContent(
`
<ion-input label="my label" counter="true" helper-text="helper text"></ion-input>
<ion-input label="Label" counter="true" helper-text="helper text"></ion-input>
`,
config
);
@@ -193,7 +222,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('default formatter should be used', async ({ page }) => {
await page.setContent(
`
<ion-input label="my label" counter="true" maxlength="20"></ion-input>
<ion-input label="Label" counter="true" maxlength="20"></ion-input>
`,
config
);
@@ -203,8 +232,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('custom formatter should be used when provided', async ({ page }) => {
await page.setContent(
`
<ion-input label="my label" counter="true" maxlength="20"></ion-input>
<ion-input label="Label" counter="true" maxlength="20"></ion-input>
<script>
const input = document.querySelector('ion-input');
input.counterFormatter = (inputLength, maxLength) => {
@@ -229,13 +257,31 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
});
});
test.describe('input: counter rendering', () => {
test.describe('regular inputs', () => {
test('should not have visual regressions when rendering counter', async ({ page }) => {
await page.setContent(`<ion-input counter="true" maxlength="20" label="my input"></ion-input>`, config);
test('should not have visual regressions when rendering counter', async ({ page }) => {
await page.setContent(`<ion-input counter="true" maxlength="20" label="Label"></ion-input>`, config);
const bottomEl = page.locator('ion-input .input-bottom');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-counter`));
});
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-counter`));
});
test('should not have visual regressions when rendering counter with helper text', async ({ page }) => {
await page.setContent(
`<ion-input label="Label" counter="true" maxlength="20" helper-text="Helper"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-counter-helper-text`));
});
test('should not have visual regressions when rendering counter with error text', async ({ page }) => {
await page.setContent(
`<ion-input class="ion-invalid ion-touched" label="Label" counter="true" maxlength="20" error-text="Error text"></ion-input>`,
config
);
const bottomEl = page.locator('ion-input');
await expect(bottomEl).toHaveScreenshot(screenshot(`input-counter-error-text`));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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