Skip to content

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

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion core/src/components/select/select.md.outline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
36 changes: 24 additions & 12 deletions core/src/components/select/select.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}

Comment on lines -15 to -22
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved this down to the select icon section.

// Select Label
// ----------------------------------------------------------------

Expand All @@ -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);
}

Expand Down Expand Up @@ -61,7 +53,7 @@
}

:host(.select-expanded) .select-highlight,
:host(.ion-focused) .select-highlight {
:host(.has-focus) .select-highlight {
transform: scale(1);
}

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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);
}

Expand Down
20 changes: 18 additions & 2 deletions core/src/components/select/select.md.solid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
4 changes: 4 additions & 0 deletions core/src/components/select/select.md.vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
10 changes: 6 additions & 4 deletions core/src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
pointer-events: none;
}

:host(.ion-focused) button {
:host(.has-focus) button {
border: 2px solid #5e9ed6;
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
32 changes: 30 additions & 2 deletions core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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.
*/
Expand Down Expand Up @@ -851,10 +861,14 @@ export class Select implements ComponentInterface {
};

private onFocus = () => {
this.hasFocus = true;

this.ionFocus.emit();
};

private onBlur = () => {
this.hasFocus = false;

this.ionBlur.emit();
};

Expand Down Expand Up @@ -1089,8 +1103,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;
Expand Down Expand Up @@ -1136,6 +1162,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,
Expand Down
46 changes: 46 additions & 0 deletions core/src/components/select/test/basic/select.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions core/src/components/select/test/color/select.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`,
Expand All @@ -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>
`,
Expand All @@ -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>
`,
Expand Down
Loading
Loading