Skip to content

Commit e69847a

Browse files
feat(select): add cancelIcon prop to modal interface
1 parent da75baf commit e69847a

8 files changed

Lines changed: 131 additions & 8 deletions

File tree

core/api.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,6 +2247,7 @@ ion-segment-view,prop,swipeGesture,boolean,true,false,false
22472247
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
22482248

22492249
ion-select,shadow
2250+
ion-select,prop,cancelIcon,boolean,false,false,false
22502251
ion-select,prop,cancelText,string,'Cancel',false,false
22512252
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
22522253
ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
@@ -2339,6 +2340,7 @@ ion-select,part,text
23392340
ion-select,part,wrapper
23402341

23412342
ion-select-modal,scoped
2343+
ion-select-modal,prop,cancelIcon,boolean,false,false,false
23422344
ion-select-modal,prop,cancelText,string,'Close',false,false
23432345
ion-select-modal,prop,header,string | undefined,undefined,false,false
23442346
ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3684,6 +3684,11 @@ export namespace Components {
36843684
"swipeGesture": boolean;
36853685
}
36863686
interface IonSelect {
3687+
/**
3688+
* If `true`, the cancel button will display an icon instead of the `cancelText`. Only applies when `interface` is set to `"modal"`. Has no effect on `"action-sheet"`, `"alert"`, or `"popover"` interfaces. When `cancelIcon` is `true`, the `cancelText` property is ignored for display but is used as the accessible label for the icon button.
3689+
* @default false
3690+
*/
3691+
"cancelIcon": boolean;
36873692
/**
36883693
* The text to display on the cancel button.
36893694
* @default 'Cancel'
@@ -3800,6 +3805,11 @@ export namespace Components {
38003805
"value"?: any | null;
38013806
}
38023807
interface IonSelectModal {
3808+
/**
3809+
* If `true`, the cancel button will display a close icon instead of the `cancelText`. When `cancelIcon` is `true`, the `cancelText` property is ignored.
3810+
* @default false
3811+
*/
3812+
"cancelIcon": boolean;
38033813
/**
38043814
* The text to display on the cancel button.
38053815
* @default 'Close'
@@ -9749,6 +9759,11 @@ declare namespace LocalJSX {
97499759
"swipeGesture"?: boolean;
97509760
}
97519761
interface IonSelect {
9762+
/**
9763+
* If `true`, the cancel button will display an icon instead of the `cancelText`. Only applies when `interface` is set to `"modal"`. Has no effect on `"action-sheet"`, `"alert"`, or `"popover"` interfaces. When `cancelIcon` is `true`, the `cancelText` property is ignored for display but is used as the accessible label for the icon button.
9764+
* @default false
9765+
*/
9766+
"cancelIcon"?: boolean;
97529767
/**
97539768
* The text to display on the cancel button.
97549769
* @default 'Cancel'
@@ -9884,6 +9899,11 @@ declare namespace LocalJSX {
98849899
"value"?: any | null;
98859900
}
98869901
interface IonSelectModal {
9902+
/**
9903+
* If `true`, the cancel button will display a close icon instead of the `cancelText`. When `cancelIcon` is `true`, the `cancelText` property is ignored.
9904+
* @default false
9905+
*/
9906+
"cancelIcon"?: boolean;
98879907
/**
98889908
* The text to display on the cancel button.
98899909
* @default 'Close'
@@ -11237,6 +11257,7 @@ declare namespace LocalJSX {
1123711257
}
1123811258
interface IonSelectAttributes {
1123911259
"cancelText": string;
11260+
"cancelIcon": boolean;
1124011261
"color": Color;
1124111262
"compareWith": string | SelectCompareFn | null;
1124211263
"disabled": boolean;
@@ -11263,6 +11284,7 @@ declare namespace LocalJSX {
1126311284
interface IonSelectModalAttributes {
1126411285
"header": string;
1126511286
"cancelText": string;
11287+
"cancelIcon": boolean;
1126611288
"multiple": boolean;
1126711289
}
1126811290
interface IonSelectOptionAttributes {

core/src/components/select-modal/select-modal.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { getIonMode } from '@global/ionic-global';
1+
import xRegular from '@phosphor-icons/core/assets/regular/x.svg';
2+
import { getIonMode, getIonTheme } from '@global/ionic-global';
23
import type { ComponentInterface } from '@stencil/core';
34
import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core';
45
import { safeCall } from '@utils/overlays';
56
import { getClassMap, hostContext } from '@utils/theme';
7+
import { closeOutline, closeSharp } from 'ionicons/icons';
68

79
import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface';
810
import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface';
@@ -28,6 +30,12 @@ export class SelectModal implements ComponentInterface {
2830
*/
2931
@Prop() cancelText = 'Close';
3032

33+
/**
34+
* If `true`, the cancel button will display a close icon instead of the `cancelText`.
35+
* When `cancelIcon` is `true`, the `cancelText` property is ignored.
36+
*/
37+
@Prop() cancelIcon = false;
38+
3139
@Prop() multiple?: boolean;
3240

3341
@Prop() options: SelectModalOption[] = [];
@@ -79,6 +87,16 @@ export class SelectModal implements ComponentInterface {
7987
}
8088
}
8189

90+
private get cancelButtonIcon(): string {
91+
const theme = getIonTheme(this);
92+
const icons: Record<string, string> = {
93+
ios: closeOutline,
94+
md: closeSharp,
95+
ionic: xRegular,
96+
};
97+
return icons[theme] ?? icons.md;
98+
}
99+
82100
private getModalContextClasses() {
83101
const el = this.el;
84102
return {
@@ -167,7 +185,16 @@ export class SelectModal implements ComponentInterface {
167185
{this.header !== undefined && <ion-title>{this.header}</ion-title>}
168186

169187
<ion-buttons slot="end">
170-
<ion-button onClick={() => this.closeModal()}>{this.cancelText}</ion-button>
188+
<ion-button
189+
aria-label={this.cancelIcon ? this.cancelText : undefined}
190+
onClick={() => this.closeModal()}
191+
>
192+
{this.cancelIcon ? (
193+
<ion-icon aria-hidden="true" icon={this.cancelButtonIcon}></ion-icon>
194+
) : (
195+
this.cancelText
196+
)}
197+
</ion-button>
171198
</ion-buttons>
172199
</ion-toolbar>
173200
</ion-header>

core/src/components/select-modal/test/custom/select-modal.e2e.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,65 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
4141
// Verify the cancel button text has been updated
4242
await expect(cancelButton).toHaveText('Close me');
4343
});
44+
45+
test('should render an icon on the cancel button when cancelIcon is true', async () => {
46+
await selectModalPage.setup(config, options, false);
47+
48+
const cancelButton = selectModalPage.selectModal.locator('ion-button');
49+
50+
// Verify no icon is shown by default
51+
await expect(cancelButton.locator('ion-icon')).not.toBeAttached();
52+
53+
await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => {
54+
selectModal.cancelIcon = true;
55+
});
56+
57+
// Verify the icon is now rendered
58+
await expect(cancelButton.locator('ion-icon')).toBeAttached();
59+
});
60+
61+
test('should use cancelText as aria-label on the cancel button when cancelIcon is true', async () => {
62+
await selectModalPage.setup(config, options, false);
63+
64+
const cancelButton = selectModalPage.selectModal.locator('ion-button');
65+
66+
await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => {
67+
selectModal.cancelIcon = true;
68+
selectModal.cancelText = 'Dismiss';
69+
});
70+
71+
await expect(cancelButton).toHaveAttribute('aria-label', 'Dismiss');
72+
});
73+
74+
test('should not set aria-label on the cancel button when cancelIcon is false', async () => {
75+
await selectModalPage.setup(config, options, false);
76+
77+
const cancelButton = selectModalPage.selectModal.locator('ion-button');
78+
79+
await expect(cancelButton).not.toHaveAttribute('aria-label');
80+
});
81+
});
82+
});
83+
84+
/**
85+
* Visual regression tests for cancelIcon across all themes.
86+
*/
87+
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
88+
test.describe(title('select-modal: cancel icon'), () => {
89+
let selectModalPage: SelectModalPage;
90+
91+
test.beforeEach(async ({ page }) => {
92+
selectModalPage = new SelectModalPage(page);
93+
});
94+
95+
test('should not have visual regressions with cancelIcon', async () => {
96+
await selectModalPage.setup(config, options, false);
97+
98+
await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => {
99+
selectModal.cancelIcon = true;
100+
});
101+
102+
await selectModalPage.screenshot(screenshot, 'select-modal-cancel-icon-diff');
103+
});
44104
});
45105
});

core/src/components/select/select.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ export class Select implements ComponentInterface {
101101
*/
102102
@Prop() cancelText = 'Cancel';
103103

104+
/**
105+
* If `true`, the cancel button will display an icon instead of the `cancelText`.
106+
* Only applies when `interface` is set to `"modal"`. Has no effect on `"action-sheet"`,
107+
* `"alert"`, or `"popover"` interfaces.
108+
* When `cancelIcon` is `true`, the `cancelText` property is ignored for display
109+
* but is used as the accessible label for the icon button.
110+
*/
111+
@Prop() cancelIcon = false;
112+
104113
/**
105114
* The color to use from your application's color palette.
106115
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -815,6 +824,7 @@ export class Select implements ComponentInterface {
815824
componentProps: {
816825
header: interfaceOptions.header,
817826
cancelText: this.cancelText,
827+
cancelIcon: this.cancelIcon,
818828
multiple,
819829
value,
820830
options: this.createOverlaySelectOptions(this.childOpts, value),

packages/angular/src/directives/proxies.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,15 +2182,15 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
21822182

21832183

21842184
@ProxyCmp({
2185-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'],
2185+
inputs: ['cancelIcon', 'cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'],
21862186
methods: ['open']
21872187
})
21882188
@Component({
21892189
selector: 'ion-select',
21902190
changeDetection: ChangeDetectionStrategy.OnPush,
21912191
template: '<ng-content></ng-content>',
21922192
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2193-
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'],
2193+
inputs: ['cancelIcon', 'cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'],
21942194
})
21952195
export class IonSelect {
21962196
protected el: HTMLIonSelectElement;
@@ -2231,14 +2231,14 @@ This event will not emit when programmatically setting the `value` property.
22312231

22322232

22332233
@ProxyCmp({
2234-
inputs: ['cancelText', 'header', 'multiple', 'options']
2234+
inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options']
22352235
})
22362236
@Component({
22372237
selector: 'ion-select-modal',
22382238
changeDetection: ChangeDetectionStrategy.OnPush,
22392239
template: '<ng-content></ng-content>',
22402240
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2241-
inputs: ['cancelText', 'header', 'multiple', 'options'],
2241+
inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'],
22422242
})
22432243
export class IonSelectModal {
22442244
protected el: HTMLIonSelectModalElement;

packages/angular/standalone/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,14 +1962,14 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
19621962

19631963
@ProxyCmp({
19641964
defineCustomElementFn: defineIonSelectModal,
1965-
inputs: ['cancelText', 'header', 'multiple', 'options']
1965+
inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options']
19661966
})
19671967
@Component({
19681968
selector: 'ion-select-modal',
19691969
changeDetection: ChangeDetectionStrategy.OnPush,
19701970
template: '<ng-content></ng-content>',
19711971
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1972-
inputs: ['cancelText', 'header', 'multiple', 'options'],
1972+
inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'],
19731973
standalone: true
19741974
})
19751975
export class IonSelectModal {

packages/vue/src/proxies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ export const IonSegmentView: StencilVueComponent<JSX.IonSegmentView> = /*@__PURE
949949

950950
export const IonSelect: StencilVueComponent<JSX.IonSelect, JSX.IonSelect["value"]> = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSelect["value"]>('ion-select', defineIonSelect, [
951951
'cancelText',
952+
'cancelIcon',
952953
'color',
953954
'compareWith',
954955
'disabled',
@@ -991,6 +992,7 @@ export const IonSelect: StencilVueComponent<JSX.IonSelect, JSX.IonSelect["value"
991992
export const IonSelectModal: StencilVueComponent<JSX.IonSelectModal> = /*@__PURE__*/ defineContainer<JSX.IonSelectModal>('ion-select-modal', defineIonSelectModal, [
992993
'header',
993994
'cancelText',
995+
'cancelIcon',
994996
'multiple',
995997
'options'
996998
]);

0 commit comments

Comments
 (0)