Skip to content

Commit 95e28b6

Browse files
feat(select): add props to customize toggle icons (#27648)
Issue number: resolves #17248 --------- <!-- 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. --> While the `icon` shadow part allows customization of the existing toggle icon, developers do not have a way to specify a different icon to use entirely. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> New props `toggleIcon` and `expandedIcon` added. (Design docs are [here](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/select/0002-custom-icons.md) and [here](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/select/0003-custom-icon-on-open.md) respectively.) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Docs PR: ionic-team/ionic-docs#2996 Dev build: `7.0.15-dev.11687278023.161b97d8` --------- Co-authored-by: ionitron <[email protected]>
1 parent 8179366 commit 95e28b6

14 files changed

+151
-9
lines changed

angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,15 +1954,15 @@ export declare interface IonSegmentButton extends Components.IonSegmentButton {}
19541954

19551955

19561956
@ProxyCmp({
1957-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'],
1957+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
19581958
methods: ['open']
19591959
})
19601960
@Component({
19611961
selector: 'ion-select',
19621962
changeDetection: ChangeDetectionStrategy.OnPush,
19631963
template: '<ng-content></ng-content>',
19641964
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1965-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'],
1965+
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
19661966
})
19671967
export class IonSelect {
19681968
protected el: HTMLElement;

core/api.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ ion-select,prop,cancelText,string,'Cancel',false,false
12381238
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
12391239
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
12401240
ion-select,prop,disabled,boolean,false,false,false
1241+
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
12411242
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
12421243
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
12431244
ion-select,prop,interfaceOptions,any,{},false,false
@@ -1252,6 +1253,7 @@ ion-select,prop,okText,string,'OK',false,false
12521253
ion-select,prop,placeholder,string | undefined,undefined,false,false
12531254
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
12541255
ion-select,prop,shape,"round" | undefined,undefined,false,false
1256+
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
12551257
ion-select,prop,value,any,undefined,false,false
12561258
ion-select,method,open,open(event?: UIEvent) => Promise<any>
12571259
ion-select,event,ionBlur,void,true

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,10 @@ export namespace Components {
26812681
* If `true`, the user cannot interact with the select.
26822682
*/
26832683
"disabled": boolean;
2684+
/**
2685+
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
2686+
*/
2687+
"expandedIcon"?: string;
26842688
/**
26852689
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
26862690
*/
@@ -2742,6 +2746,10 @@ export namespace Components {
27422746
* The shape of the select. If "round" it will have an increased border radius.
27432747
*/
27442748
"shape"?: 'round';
2749+
/**
2750+
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode, or `caretDownSharp` for `md` mode.
2751+
*/
2752+
"toggleIcon"?: string;
27452753
/**
27462754
* The value of the select.
27472755
*/
@@ -6755,6 +6763,10 @@ declare namespace LocalJSX {
67556763
* If `true`, the user cannot interact with the select.
67566764
*/
67576765
"disabled"?: boolean;
6766+
/**
6767+
* The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed.
6768+
*/
6769+
"expandedIcon"?: string;
67586770
/**
67596771
* The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode.
67606772
*/
@@ -6835,6 +6847,10 @@ declare namespace LocalJSX {
68356847
* The shape of the select. If "round" it will have an increased border radius.
68366848
*/
68376849
"shape"?: 'round';
6850+
/**
6851+
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode, or `caretDownSharp` for `md` mode.
6852+
*/
6853+
"toggleIcon"?: string;
68386854
/**
68396855
* The value of the select.
68406856
*/

core/src/components/select/select.md.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
* when the select is activated.
9595
* This should only happen on MD.
9696
*/
97-
:host(.select-expanded:not(.legacy-select)) .select-icon {
97+
:host(.select-expanded:not(.legacy-select):not(.has-expanded-icon)) .select-icon {
9898
@include transform(rotate(180deg));
9999
}
100100

@@ -123,16 +123,16 @@
123123
@include transform(translate3d(0, -9px, 0));
124124
}
125125

126-
:host-context(.item-has-focus) .select-icon {
126+
:host-context(.item-has-focus):host(:not(.has-expanded-icon)) .select-icon {
127127
@include transform(rotate(180deg));
128128
}
129129

130130
/**
131131
* Ensure that the translation we did
132132
* above is preserved when we rotate the select icon.
133133
*/
134-
:host-context(.item-has-focus.item-label-stacked) .select-icon,
135-
:host-context(.item-has-focus.item-label-floating:not(.item-fill-outline)) .select-icon {
134+
:host-context(.item-has-focus.item-label-stacked):host(:not(.has-expanded-icon)) .select-icon,
135+
:host-context(.item-has-focus.item-label-floating:not(.item-fill-outline)):host(:not(.has-expanded-icon)) .select-icon {
136136
@include transform(rotate(180deg), translate3d(0, -9px, 0));
137137
}
138138

core/src/components/select/select.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,19 @@ export class Select implements ComponentInterface {
183183
*/
184184
@Prop() selectedText?: string | null;
185185

186+
/**
187+
* The toggle icon to use. Defaults to `chevronExpand` for `ios` mode,
188+
* or `caretDownSharp` for `md` mode.
189+
*/
190+
@Prop() toggleIcon?: string;
191+
192+
/**
193+
* The toggle icon to show when the select is open. If defined, the icon
194+
* rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon`
195+
* will be used for when the select is both open and closed.
196+
*/
197+
@Prop() expandedIcon?: string;
198+
186199
/**
187200
* The shape of the select. If "round" it will have an increased border radius.
188201
*/
@@ -820,7 +833,8 @@ export class Select implements ComponentInterface {
820833
}
821834

822835
private renderSelect() {
823-
const { disabled, el, isExpanded, labelPlacement, justify, placeholder, fill, shape, name, value } = this;
836+
const { disabled, el, isExpanded, expandedIcon, labelPlacement, justify, placeholder, fill, shape, name, value } =
837+
this;
824838
const mode = getIonMode(this);
825839
const hasFloatingOrStackedLabel = labelPlacement === 'floating' || labelPlacement === 'stacked';
826840
const justifyEnabled = !hasFloatingOrStackedLabel;
@@ -839,6 +853,7 @@ export class Select implements ComponentInterface {
839853
'in-item-color': hostContext('ion-item.ion-color', el),
840854
'select-disabled': disabled,
841855
'select-expanded': isExpanded,
856+
'has-expanded-icon': expandedIcon !== undefined,
842857
'has-value': this.hasValue(),
843858
'has-placeholder': placeholder !== undefined,
844859
'ion-focusable': true,
@@ -893,7 +908,7 @@ Developers can use the "legacy" property to continue using the legacy form marku
893908
this.hasLoggedDeprecationWarning = true;
894909
}
895910

896-
const { disabled, el, inputId, isExpanded, name, placeholder, value } = this;
911+
const { disabled, el, inputId, isExpanded, expandedIcon, name, placeholder, value } = this;
897912
const mode = getIonMode(this);
898913
const { labelText, labelId } = getAriaLabel(el, inputId);
899914

@@ -926,6 +941,7 @@ Developers can use the "legacy" property to continue using the legacy form marku
926941
'in-item-color': hostContext('ion-item.ion-color', el),
927942
'select-disabled': disabled,
928943
'select-expanded': isExpanded,
944+
'has-expanded-icon': expandedIcon !== undefined,
929945
'legacy-select': true,
930946
}}
931947
>
@@ -974,7 +990,16 @@ Developers can use the "legacy" property to continue using the legacy form marku
974990
*/
975991
private renderSelectIcon() {
976992
const mode = getIonMode(this);
977-
const icon = mode === 'ios' ? chevronExpand : caretDownSharp;
993+
const { isExpanded, toggleIcon, expandedIcon } = this;
994+
let icon: string;
995+
996+
if (isExpanded && expandedIcon !== undefined) {
997+
icon = expandedIcon;
998+
} else {
999+
const defaultIcon = mode === 'ios' ? chevronExpand : caretDownSharp;
1000+
icon = toggleIcon ?? defaultIcon;
1001+
}
1002+
9781003
return <ion-icon class="select-icon" part="icon" aria-hidden="true" icon={icon}></ion-icon>;
9791004
}
9801005

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Select - toggleIcon</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
</head>
16+
17+
<body>
18+
<ion-app>
19+
<ion-header>
20+
<ion-toolbar>
21+
<ion-title>Select - toggleIcon</ion-title>
22+
</ion-toolbar>
23+
</ion-header>
24+
25+
<ion-content class="test-content">
26+
<ion-list>
27+
<ion-item>
28+
<ion-select label="toggleIcon" toggle-icon="arrow-down" placeholder="Select one" interface="popover">
29+
<ion-select-option value="apples">Apples</ion-select-option>
30+
<ion-select-option value="oranges">Oranges</ion-select-option>
31+
<ion-select-option value="pears">Pears</ion-select-option>
32+
</ion-select>
33+
</ion-item>
34+
<ion-item>
35+
<ion-select label="expandedIcon" expanded-icon="arrow-up" placeholder="Select one" interface="popover">
36+
<ion-select-option value="apples">Apples</ion-select-option>
37+
<ion-select-option value="oranges">Oranges</ion-select-option>
38+
<ion-select-option value="pears">Pears</ion-select-option>
39+
</ion-select>
40+
</ion-item>
41+
<ion-item>
42+
<ion-select
43+
label="Both"
44+
toggle-icon="arrow-down"
45+
expanded-icon="pizza"
46+
placeholder="Select one"
47+
interface="popover"
48+
>
49+
<ion-select-option value="apples">Apples</ion-select-option>
50+
<ion-select-option value="oranges">Oranges</ion-select-option>
51+
<ion-select-option value="pears">Pears</ion-select-option>
52+
</ion-select>
53+
</ion-item>
54+
</ion-list>
55+
</ion-content>
56+
</ion-app>
57+
</body>
58+
</html>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, config }) => {
5+
test.describe(title('select: toggleIcon'), () => {
6+
test('should render a custom toggleIcon', async ({ page }) => {
7+
await page.setContent(
8+
`
9+
<ion-select toggle-icon="pizza" label="Select" value="a">
10+
<ion-select-option value="a">Apple</ion-select-option>
11+
</ion-select>
12+
`,
13+
config
14+
);
15+
16+
const select = page.locator('ion-select');
17+
await expect(select).toHaveScreenshot(screenshot(`select-toggle-icon`));
18+
});
19+
20+
test('should render a custom expandedIcon', async ({ page }) => {
21+
await page.setContent(
22+
`
23+
<ion-select expanded-icon="pizza" interface="popover" label="Select" value="a">
24+
<ion-select-option value="a">Apple</ion-select-option>
25+
</ion-select>
26+
`,
27+
config
28+
);
29+
30+
const select = page.locator('ion-select');
31+
const popoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
32+
33+
await select.click();
34+
await popoverDidPresent.next();
35+
36+
await expect(select).toHaveScreenshot(screenshot(`select-expanded-icon`));
37+
});
38+
});
39+
});

packages/vue/src/proxies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,8 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
739739
'okText',
740740
'placeholder',
741741
'selectedText',
742+
'toggleIcon',
743+
'expandedIcon',
742744
'shape',
743745
'value',
744746
'ionChange',

0 commit comments

Comments
 (0)