fix(select): update icon color and use correct focused class (#30342)

Issue number: 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?
<!-- Please describe the current behavior that you are modifying. -->

There are a few items to note:
- Styles are using a non-existing focus class. It currently uses
`.has-focus` which isn't being used anywhere within `select.tsx`. It
seems that `.has-focus` comes a copy and paste that wasn't updated.
- Icon uses the highlight color when expanded, in item, and no fill.
This leads to the styling looking weird compared to when it's not in an
item especially since labels do not change colors. Only applies to `md`
mode.

| List no lines | List with lines |
| --- | ----------- |
| ![Screenshot 2025-04-07 at 1 01
14 PM](https://github.com/user-attachments/assets/e310e5e8-d0bc-4976-b623-c8db358307c8)
| ![Screenshot 2025-04-07 at 1 01
31 PM](https://github.com/user-attachments/assets/d4c0776d-cd5d-48a1-95a3-42b74e3dd767)
|

- The focus without a validation status does not update the border
correctly when inside an item and has a solid fill. Only applies to `md`
mode.

| Outside item | Inside item |
|--------|--------|
| ![Screenshot 2025-04-07 at 1 07
13 PM](https://github.com/user-attachments/assets/ee8c4fff-630d-4f7c-b3c5-f9daf22ff7d4)
| ![Screenshot 2025-04-07 at 1 08
45 PM](https://github.com/user-attachments/assets/046d69fc-823e-4fa4-a19f-f8e641874b4d)
|

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Styles are now using the correct focus class: `.has-focus`, removing
`ion-focused`.
- If the select is inside an item and has no fill then the icon will not
use the highlight color. Only applies to `md` mode.


| List no lines | List with lines |
| --- | ----------- |
| ![Screenshot 2025-04-07 at 1 13
17 PM](https://github.com/user-attachments/assets/0e6fd28e-4925-4799-a24b-8e21348eb168)
| ![Screenshot 2025-04-07 at 1 14
11 PM](https://github.com/user-attachments/assets/adec2576-27d9-4150-8e60-8af5fa9cc012)
|

- The focus without a validation status uses the highlight color when
inside an item and has a solid fill. Only applies to `md` mode.

| Outside item | Inside item |
|--------|--------|
| ![Screenshot 2025-04-07 at 1 07
13 PM](https://github.com/user-attachments/assets/ee8c4fff-630d-4f7c-b3c5-f9daf22ff7d4)
| ![Screenshot 2025-04-07 at 1 16
47 PM](https://github.com/user-attachments/assets/bc8b03b9-6ad1-40b0-a7fd-3f6e654ec066)
|

## 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. -->

Dev build: `8.5.4-dev.11744743162.1ec9251d`

Select cannot have a focused class and an expanded class at the same
time. It's one or the other.

---------

Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
Maria Hutt
2025-04-16 10:16:54 -07:00
committed by GitHub
parent d52fca084c
commit cad1c61528
57 changed files with 453 additions and 117 deletions

View File

@@ -50,11 +50,20 @@ export class Input implements ComponentInterface {
* Resets when the input loses focus.
*/
private didInputClearOnEdit = false;
/**
* The value of the input when the input is focused.
*/
private focusedValue?: string | number | null;
/**
* The `hasFocus` state ensures the focus class is
* added regardless of how the element is focused.
* The `ion-focused` class only applies when focused
* via tabbing, not by clicking.
* The `has-focus` logic was added to ensure the class
* is applied in both cases.
*/
@State() hasFocus = false;
@Element() el!: HTMLIonInputElement;

View File

@@ -43,7 +43,7 @@
* the select is focused.
*/
:host(.select-fill-outline.select-expanded),
:host(.select-fill-outline.ion-focused) {
:host(.select-fill-outline.has-focus) {
--border-width: var(--highlight-height);
--border-color: var(--highlight-color);
}
@@ -240,3 +240,18 @@
:host(.label-floating.select-fill-outline) .select-outline-notch {
border-top: none;
}
// Select Icon
// ----------------------------------------------------------------
/**
* When the select has an outline fill and
* in an item, then the icon should
* take on the highlight color.
*/
:host(.in-item.select-expanded.select-fill-outline) .select-wrapper .select-icon,
:host(.in-item.has-focus.select-fill-outline) .select-wrapper .select-icon,
:host(.in-item.has-focus.ion-valid.select-fill-outline) .select-wrapper .select-icon,
:host(.in-item.ion-touched.ion-invalid.select-fill-outline) .select-wrapper .select-icon {
color: var(--highlight-color);
}

View File

@@ -12,14 +12,6 @@
--highlight-height: 2px;
}
.select-icon {
width: $select-md-icon-size;
transition: transform .15s cubic-bezier(.4, 0, .2, 1);
color: #{$text-color-step-500};
}
// Select Label
// ----------------------------------------------------------------
@@ -29,9 +21,9 @@
* only apply to floating or stacked labels.
*/
:host(.select-label-placement-floating.select-expanded) .label-text-wrapper,
:host(.select-label-placement-floating.ion-focused) .label-text-wrapper,
:host(.select-label-placement-floating.has-focus) .label-text-wrapper,
:host(.select-label-placement-stacked.select-expanded) .label-text-wrapper,
:host(.select-label-placement-stacked.ion-focused) .label-text-wrapper {
:host(.select-label-placement-stacked.has-focus) .label-text-wrapper {
color: var(--highlight-color);
}
@@ -61,7 +53,7 @@
}
:host(.select-expanded) .select-highlight,
:host(.ion-focused) .select-highlight {
:host(.has-focus) .select-highlight {
transform: scale(1);
}
@@ -77,6 +69,14 @@
// Select Icon
// ----------------------------------------------------------------
.select-icon {
width: $select-md-icon-size;
transition: transform .15s cubic-bezier(.4, 0, .2, 1);
color: #{$select-md-icon-color};
}
/**
* This rotates the chevron icon
* when the select is activated.
@@ -86,6 +86,18 @@
@include transform(rotate(180deg));
}
/**
* When the select has no fill and
* in an item, then the icon should
* be the same color as the text color.
*/
:host(.in-item.select-expanded) .select-wrapper .select-icon,
:host(.in-item.has-focus) .select-wrapper .select-icon,
:host(.in-item.has-focus.ion-valid) .select-wrapper .select-icon,
:host(.in-item.ion-touched.ion-invalid) .select-wrapper .select-icon {
color: #{$select-md-icon-color};
}
/**
* When the select is focused the icon should
* take on the highlight color.
@@ -95,7 +107,7 @@
:host(.select-expanded) .select-wrapper .select-icon,
:host(.has-focus.ion-valid) .select-wrapper .select-icon,
:host(.ion-touched.ion-invalid) .select-wrapper .select-icon,
:host(.ion-focused) .select-wrapper .select-icon {
:host(.has-focus) .select-wrapper .select-icon {
color: var(--highlight-color);
}

View File

@@ -27,6 +27,7 @@
* If the select has a validity state, the
* border should reflect that as a color.
*/
:host(.select-expanded.select-fill-solid.ion-valid),
:host(.has-focus.select-fill-solid.ion-valid),
:host(.select-fill-solid.ion-touched.ion-invalid) {
--border-color: var(--highlight-color);
@@ -56,9 +57,9 @@
* much darker on focus.
*/
:host(.select-fill-solid.select-expanded),
:host(.select-fill-solid.ion-focused) {
:host(.select-fill-solid.has-focus) {
--background: #{$background-color-step-150};
--border-color: #{$background-color-step-750};
--border-color: var(--highlight-color);
}
:host(.select-fill-solid) .select-wrapper {
@@ -79,3 +80,18 @@
*/
max-width: calc(100% / #{$form-control-label-stacked-scale});
}
// Select Icon
// ----------------------------------------------------------------
/**
* When the select has a solid fill and
* in an item, then the icon should
* take on the highlight color.
*/
:host(.in-item.select-expanded.select-fill-solid) .select-wrapper .select-icon,
:host(.in-item.has-focus.select-fill-solid) .select-wrapper .select-icon,
:host(.in-item.has-focus.ion-valid.select-fill-solid) .select-wrapper .select-icon,
:host(.in-item.ion-touched.ion-invalid.select-fill-solid) .select-wrapper .select-icon {
color: var(--highlight-color);
}

View File

@@ -6,6 +6,10 @@
/// @prop - Size of the select icon
$select-md-icon-size: dynamic-font(13px);
/// @prop - Color of the select icon
$select-md-icon-color: $text-color-step-500;
/// @prop - The amount of whitespace to display on either side of the floating label
$select-md-floating-label-padding: 4px;

View File

@@ -88,7 +88,7 @@
pointer-events: none;
}
:host(.ion-focused) button {
:host(.has-focus) button {
border: 2px solid #5e9ed6;
}
@@ -308,7 +308,9 @@ button {
* highlight when the select is blurred.
*/
:host(.has-focus.ion-valid),
:host(.ion-touched.ion-invalid) {
:host(.select-expanded.ion-valid),
:host(.ion-touched.ion-invalid),
:host(.select-expanded.ion-touched.ion-invalid) {
--border-color: var(--highlight-color);
}
@@ -320,7 +322,7 @@ button {
* present on the select. Otherwise the helper text should
* be shown.
*/
.select-bottom .error-text {
.select-bottom .error-text {
display: none;
color: var(--highlight-color-invalid);
@@ -597,7 +599,7 @@ button {
* :host(.label-floating.select-label-placement-floating) .native-wrapper .select-placeholder
*/
:host(.select-expanded.select-label-placement-floating) .native-wrapper .select-placeholder,
:host(.ion-focused.select-label-placement-floating) .native-wrapper .select-placeholder,
:host(.has-focus.select-label-placement-floating) .native-wrapper .select-placeholder,
:host(.has-value.select-label-placement-floating) .native-wrapper .select-placeholder {
opacity: 1;
}

View File

@@ -71,6 +71,16 @@ export class Select implements ComponentInterface {
@State() isExpanded = false;
/**
* The `hasFocus` state ensures the focus class is
* added regardless of how the element is focused.
* The `ion-focused` class only applies when focused
* via tabbing, not by clicking.
* The `has-focus` logic was added to ensure the class
* is applied in both cases.
*/
@State() hasFocus = false;
/**
* The text to display on the cancel button.
*/
@@ -852,10 +862,14 @@ export class Select implements ComponentInterface {
};
private onFocus = () => {
this.hasFocus = true;
this.ionFocus.emit();
};
private onBlur = () => {
this.hasFocus = false;
this.ionBlur.emit();
};
@@ -1090,8 +1104,20 @@ export class Select implements ComponentInterface {
}
render() {
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
this;
const {
disabled,
el,
isExpanded,
expandedIcon,
labelPlacement,
justify,
placeholder,
fill,
shape,
name,
value,
hasFocus,
} = this;
const mode = getIonMode(this);
const hasFloatingOrStackedLabel = labelPlacement === 'floating' || labelPlacement === 'stacked';
const justifyEnabled = !hasFloatingOrStackedLabel && justify !== undefined;
@@ -1137,6 +1163,8 @@ export class Select implements ComponentInterface {
'has-value': hasValue,
'label-floating': labelShouldFloat,
'has-placeholder': placeholder !== undefined,
'has-focus': hasFocus,
// TODO(FW-6451): Remove `ion-focusable` class in favor of `has-focus`.
'ion-focusable': true,
[`select-${rtl}`]: true,
[`select-fill-${fill}`]: fill !== undefined,

View File

@@ -324,3 +324,49 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
});
});
});
/**
* focus has a consistent behavior across modes
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('select: focus'), () => {
test('should have the focus class when tabbing', async ({ page, pageUtils }) => {
await page.setContent(
`
<ion-select aria-label="Fruit" interface="alert">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
`,
config
);
const select = page.locator('ion-select');
await pageUtils.pressKeys('Tab');
await expect(select).toHaveClass(/has-focus/);
});
test('should have the focus class after clicking to close', async ({ page }) => {
await page.setContent(
`
<ion-select aria-label="Fruit" interface="alert">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
`,
config
);
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const select = page.locator('ion-select');
const alert = page.locator('ion-alert');
const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)');
await select.click();
await ionAlertDidPresent.next();
await confirmButton.click();
await expect(select).toHaveClass(/has-focus/);
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -7,7 +7,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should set label and highlight color on expand', async ({ page }) => {
await page.setContent(
`
<ion-select label="Label" class="select-expanded" value="apple" class="ion-focused" color="danger">
<ion-select label="Label" class="select-expanded" value="apple" class="has-focus" color="danger">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
`,
@@ -22,7 +22,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should set label and highlight color on expand', async ({ page }) => {
await page.setContent(
`
<ion-select fill="solid" label="Label" class="select-expanded" value="apple" class="ion-focused" color="danger">
<ion-select fill="solid" label="Label" class="select-expanded" value="apple" class="has-focus" color="danger">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
`,
@@ -37,7 +37,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should set label and highlight color on expand', async ({ page }) => {
await page.setContent(
`
<ion-select fill="outline" label="Label" class="select-expanded" value="apple" class="ion-focused" color="danger">
<ion-select fill="outline" label="Label" class="select-expanded" value="apple" class="has-focus" color="danger">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
`,

View File

@@ -56,14 +56,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select label-placement="start" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select label-placement="start" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select label-placement="start" class="ion-valid ion-focused" label="Fruit">
<ion-select label-placement="start" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -80,14 +80,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select label-placement="floating" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select label-placement="floating" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select label-placement="floating" class="ion-valid ion-focused" label="Fruit">
<ion-select label-placement="floating" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -104,14 +104,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select label-placement="stacked" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select label-placement="stacked" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select label-placement="stacked" class="ion-valid ion-focused" label="Fruit">
<ion-select label-placement="stacked" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -128,14 +128,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select label-placement="start" value="hi@ionic.io" class="custom ion-focused" label="Fruit">
<ion-select label-placement="start" value="hi@ionic.io" class="custom has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select label-placement="start" class="custom ion-valid ion-focused" label="Fruit">
<ion-select label-placement="start" class="custom ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -152,14 +152,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="solid" label-placement="start" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="start" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="solid" label-placement="start" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="start" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -176,14 +176,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="solid" label-placement="floating" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="floating" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="solid" label-placement="floating" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="floating" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -200,14 +200,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="solid" label-placement="stacked" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="stacked" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="solid" label-placement="stacked" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="stacked" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -224,20 +224,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select
fill="solid"
label-placement="start"
value="hi@ionic.io"
class="custom ion-focused"
label="Fruit"
>
<ion-select fill="solid" label-placement="start" value="hi@ionic.io" class="custom has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="solid" label-placement="start" class="custom ion-valid ion-focused" label="Fruit">
<ion-select fill="solid" label-placement="start" class="custom ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -254,14 +248,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="outline" label-placement="start" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="start" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="outline" label-placement="start" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="start" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -278,14 +272,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="outline" label-placement="floating" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="floating" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="outline" label-placement="floating" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="floating" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -302,14 +296,14 @@
<div class="grid">
<div class="grid-item">
<h2>Focus</h2>
<ion-select fill="outline" label-placement="stacked" value="hi@ionic.io" class="ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="stacked" value="hi@ionic.io" class="has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="outline" label-placement="stacked" class="ion-valid ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="stacked" class="ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -330,7 +324,7 @@
fill="outline"
label-placement="start"
value="hi@ionic.io"
class="custom ion-focused"
class="custom has-focus"
label="Fruit"
>
<ion-select-option value="apple">Apple</ion-select-option>
@@ -339,7 +333,7 @@
<div class="grid-item">
<h2>Valid, Focus</h2>
<ion-select fill="outline" label-placement="start" class="custom ion-valid ion-focused" label="Fruit">
<ion-select fill="outline" label-placement="start" class="custom ion-valid has-focus" label="Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>

View File

@@ -7,41 +7,114 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should render valid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select value="apple" class="ion-valid ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-no-fill-valid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-no-fill-valid`));
});
test('should render invalid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-no-fill-invalid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-no-fill-invalid`));
});
test('should render invalid with focus state correctly', async ({ page }) => {
await page.setContent(
`
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select value="apple" class="ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select value="apple" class="ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-no-fill-invalid-focus`));
});
test('should render focused state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select value="apple" class="ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-no-fill-focus`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-no-fill-focus`));
});
test('should render custom highlight correctly', async ({ page }) => {
await page.setContent(
@@ -56,15 +129,15 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
</style>
<div class="container">
<ion-select value="apple" class="custom ion-valid ion-focused" label="Favorite Fruit">
<ion-select value="apple" class="custom ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select value="apple" class="custom ion-touched ion-invalid ion-focused" label="Favorite Fruit">
<ion-select value="apple" class="custom ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select value="apple" class="custom ion-focused" label="Favorite Fruit">
<ion-select value="apple" class="custom has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -80,41 +153,86 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should render valid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="solid" value="apple" class="ion-valid ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="solid" value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="solid" value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-solid-valid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-solid-valid`));
});
test('should render invalid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="solid" value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="solid" value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="solid" value="apple" class="ion-touched ion-invalid" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-solid-invalid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-solid-invalid`));
});
test('should render focused state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="solid" value="apple" class="ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="solid" value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="solid" value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-solid-focus`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-solid-focus`));
});
test('should render custom highlight correctly', async ({ page }) => {
await page.setContent(
@@ -129,15 +247,15 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
</style>
<div class="container">
<ion-select fill="solid" value="apple" class="custom ion-valid ion-focused" label="Favorite Fruit">
<ion-select fill="solid" value="apple" class="custom ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select fill="solid" value="apple" class="custom ion-touched ion-invalid ion-focused" label="Favorite Fruit">
<ion-select fill="solid" value="apple" class="custom ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select fill="solid" value="apple" class="custom ion-focused" label="Favorite Fruit">
<ion-select fill="solid" value="apple" class="custom has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -153,41 +271,86 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should render valid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="outline" value="apple" class="ion-valid ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="outline" value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="outline" value="apple" class="ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-outline-valid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-outline-valid`));
});
test('should render invalid state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="outline" value="apple" class="ion-touched ion-invalid ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="outline" value="apple" class="ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="outline" value="apple" class="ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-outline-invalid`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-outline-invalid`));
});
test('should render focused state correctly', async ({ page }) => {
await page.setContent(
`
<ion-select fill="outline" value="apple" class="ion-focused" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="outline" value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-list>
<ion-item>
<ion-select fill="outline" value="apple" class="has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-outline-focus`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-outline-focus`));
});
test('should render custom highlight correctly', async ({ page }) => {
await page.setContent(
@@ -202,15 +365,15 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
</style>
<div class="container">
<ion-select fill="outline" value="apple" class="custom ion-valid ion-focused" label="Favorite Fruit">
<ion-select fill="outline" value="apple" class="custom ion-valid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select fill="outline" value="apple" class="custom ion-touched ion-invalid ion-focused" label="Favorite Fruit">
<ion-select fill="outline" value="apple" class="custom ion-touched ion-invalid has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
<ion-select fill="outline" value="apple" class="custom ion-focused" label="Favorite Fruit">
<ion-select fill="outline" value="apple" class="custom has-focus" label="Favorite Fruit">
<ion-select-option value="apple">Apple</ion-select-option>
</ion-select>
</div>
@@ -229,39 +392,78 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
test('should render bottom highlight', async ({ page }) => {
await page.setContent(
`
<ion-select label="Label" class="select-expanded"></ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select label="Label" class="select-expanded"></ion-select>
<ion-list>
<ion-item>
<ion-select label="Label" class="select-expanded"></ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-no-fill-highlight`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-no-fill-highlight`));
});
});
test.describe('select: solid', () => {
test('should render bottom highlight', async ({ page }) => {
await page.setContent(
`
<ion-select fill="solid" label="Label" class="select-expanded"></ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="solid" label="Label" class="select-expanded"></ion-select>
<ion-list>
<ion-item>
<ion-select fill="solid" label="Label" class="select-expanded"></ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-solid-highlight`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-solid-highlight`));
});
});
test.describe('select: outline', () => {
test('should render bottom highlight', async ({ page }) => {
await page.setContent(
`
<ion-select fill="outline" label="Label" class="select-expanded"></ion-select>
<style>
#container {
display: grid;
gap: 5px;
}
</style>
<div id="container">
<ion-select fill="outline" label="Label" class="select-expanded"></ion-select>
<ion-list>
<ion-item>
<ion-select fill="outline" label="Label" class="select-expanded"></ion-select>
</ion-item>
</ion-list>
</div>
`,
config
);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(screenshot(`select-outline-highlight`));
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`select-outline-highlight`));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -70,6 +70,14 @@ export class Textarea implements ComponentInterface {
@Element() el!: HTMLIonTextareaElement;
/**
* The `hasFocus` state ensures the focus class is
* added regardless of how the element is focused.
* The `ion-focused` class only applies when focused
* via tabbing, not by clicking.
* The `has-focus` logic was added to ensure the class
* is applied in both cases.
*/
@State() hasFocus = false;
/**