diff --git a/core/api.txt b/core/api.txt index e4e1ca4d603..aedc71f7a53 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2247,6 +2247,7 @@ ion-segment-view,prop,swipeGesture,boolean,true,false,false ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true ion-select,shadow +ion-select,prop,cancelIcon,boolean,false,false,false ion-select,prop,cancelText,string,'Cancel',false,false ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false @@ -2339,6 +2340,7 @@ ion-select,part,text ion-select,part,wrapper ion-select-modal,scoped +ion-select-modal,prop,cancelIcon,boolean,false,false,false ion-select-modal,prop,cancelText,string,'Close',false,false ion-select-modal,prop,header,string | undefined,undefined,false,false ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ecbdb7475c6..4a0fa3b508e 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3684,6 +3684,11 @@ export namespace Components { "swipeGesture": boolean; } interface IonSelect { + /** + * 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. + * @default false + */ + "cancelIcon": boolean; /** * The text to display on the cancel button. * @default 'Cancel' @@ -3800,6 +3805,11 @@ export namespace Components { "value"?: any | null; } interface IonSelectModal { + /** + * If `true`, the cancel button will display a close icon instead of the `cancelText`. When `cancelIcon` is `true`, `cancelText` is not displayed visually but is still used as the accessible label (`aria-label`) for the button. + * @default false + */ + "cancelIcon": boolean; /** * The text to display on the cancel button. * @default 'Close' @@ -9749,6 +9759,11 @@ declare namespace LocalJSX { "swipeGesture"?: boolean; } interface IonSelect { + /** + * 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. + * @default false + */ + "cancelIcon"?: boolean; /** * The text to display on the cancel button. * @default 'Cancel' @@ -9884,6 +9899,11 @@ declare namespace LocalJSX { "value"?: any | null; } interface IonSelectModal { + /** + * If `true`, the cancel button will display a close icon instead of the `cancelText`. When `cancelIcon` is `true`, `cancelText` is not displayed visually but is still used as the accessible label (`aria-label`) for the button. + * @default false + */ + "cancelIcon"?: boolean; /** * The text to display on the cancel button. * @default 'Close' @@ -11237,6 +11257,7 @@ declare namespace LocalJSX { } interface IonSelectAttributes { "cancelText": string; + "cancelIcon": boolean; "color": Color; "compareWith": string | SelectCompareFn | null; "disabled": boolean; @@ -11263,6 +11284,7 @@ declare namespace LocalJSX { interface IonSelectModalAttributes { "header": string; "cancelText": string; + "cancelIcon": boolean; "multiple": boolean; } interface IonSelectOptionAttributes { diff --git a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-round-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-round-ionic-md-ltr-light-Mobile-Safari-linux.png index 91cc083d380..3bc0ee848bc 100644 Binary files a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-round-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-round-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png index f0ecfd908fd..d5c999183a4 100644 Binary files a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx index c277c194da8..14682d2e62c 100644 --- a/core/src/components/select-modal/select-modal.tsx +++ b/core/src/components/select-modal/select-modal.tsx @@ -1,8 +1,10 @@ -import { getIonMode } from '@global/ionic-global'; +import { getIonMode, getIonTheme } from '@global/ionic-global'; +import xRegular from '@phosphor-icons/core/assets/regular/x.svg'; import type { ComponentInterface } from '@stencil/core'; import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core'; import { safeCall } from '@utils/overlays'; import { getClassMap, hostContext } from '@utils/theme'; +import { closeOutline, closeSharp } from 'ionicons/icons'; import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface'; import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface'; @@ -28,6 +30,13 @@ export class SelectModal implements ComponentInterface { */ @Prop() cancelText = 'Close'; + /** + * If `true`, the cancel button will display a close icon instead of the `cancelText`. + * When `cancelIcon` is `true`, `cancelText` is not displayed visually but is still used + * as the accessible label (`aria-label`) for the button. + */ + @Prop() cancelIcon = false; + @Prop() multiple?: boolean; @Prop() options: SelectModalOption[] = []; @@ -79,6 +88,16 @@ export class SelectModal implements ComponentInterface { } } + private get cancelButtonIcon(): string { + const theme = getIonTheme(this); + const icons: Record = { + ios: closeOutline, + md: closeSharp, + ionic: xRegular, + }; + return icons[theme]; + } + private getModalContextClasses() { const el = this.el; return { @@ -167,7 +186,13 @@ export class SelectModal implements ComponentInterface { {this.header !== undefined && {this.header}} - this.closeModal()}>{this.cancelText} + this.closeModal()}> + {this.cancelIcon ? ( + + ) : ( + this.cancelText + )} + diff --git a/core/src/components/select-modal/test/basic/index.html b/core/src/components/select-modal/test/basic/index.html index 8ddb3b54f0b..3e0ac7eec55 100644 --- a/core/src/components/select-modal/test/basic/index.html +++ b/core/src/components/select-modal/test/basic/index.html @@ -23,18 +23,34 @@ - - + + + Cancel Text (default) + + + + + + + + + Cancel Icon + + + + diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts b/core/src/components/select-modal/test/custom/select-modal.e2e.ts index 7c95a4428dd..e88153cf442 100644 --- a/core/src/components/select-modal/test/custom/select-modal.e2e.ts +++ b/core/src/components/select-modal/test/custom/select-modal.e2e.ts @@ -41,5 +41,65 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { // Verify the cancel button text has been updated await expect(cancelButton).toHaveText('Close me'); }); + + test('should render an icon on the cancel button when cancelIcon is true', async () => { + await selectModalPage.setup(config, options, false); + + const cancelButton = selectModalPage.selectModal.locator('ion-button'); + + // Verify no icon is shown by default + await expect(cancelButton.locator('ion-icon')).not.toBeAttached(); + + await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => { + selectModal.cancelIcon = true; + }); + + // Verify the icon is now rendered + await expect(cancelButton.locator('ion-icon')).toBeAttached(); + }); + + test('should use cancelText as aria-label on the cancel button when cancelIcon is true', async () => { + await selectModalPage.setup(config, options, false); + + const cancelButton = selectModalPage.selectModal.locator('ion-button'); + + await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => { + selectModal.cancelIcon = true; + selectModal.cancelText = 'Dismiss'; + }); + + await expect(cancelButton).toHaveAttribute('aria-label', 'Dismiss'); + }); + + test('should not set aria-label on the cancel button when cancelIcon is false', async () => { + await selectModalPage.setup(config, options, false); + + const cancelButton = selectModalPage.selectModal.locator('ion-button'); + + await expect(cancelButton).not.toHaveAttribute('aria-label'); + }); + }); +}); + +/** + * Visual regression tests for cancelIcon across all themes. + */ +configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('select-modal: cancel icon'), () => { + let selectModalPage: SelectModalPage; + + test.beforeEach(async ({ page }) => { + selectModalPage = new SelectModalPage(page); + }); + + test('should not have visual regressions with cancelIcon', async () => { + await selectModalPage.setup(config, options, false); + + await selectModalPage.selectModal.evaluate((selectModal: HTMLIonSelectModalElement) => { + selectModal.cancelIcon = true; + }); + + await selectModalPage.screenshot(screenshot, 'select-modal-cancel-icon-diff'); + }); }); }); diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..29da072d5db Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..2af6c2f2edb Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..2b77ebfd393 Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..346a8254cf5 Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..35ae08befcc Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..57602bcd45d Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..40311725447 Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..8d0c59cfe19 Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..60c70da98bf Binary files /dev/null and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 8071434ba8c..00aa55a209d 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -101,6 +101,15 @@ export class Select implements ComponentInterface { */ @Prop() cancelText = 'Cancel'; + /** + * 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. + */ + @Prop() cancelIcon = false; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -815,6 +824,7 @@ export class Select implements ComponentInterface { componentProps: { header: interfaceOptions.header, cancelText: this.cancelText, + cancelIcon: this.cancelIcon, multiple, value, options: this.createOverlaySelectOptions(this.childOpts, value), diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index fecc4aed2a8..55e6c0134c1 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2182,7 +2182,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView { @ProxyCmp({ - 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'], + 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'], methods: ['open'] }) @Component({ @@ -2190,7 +2190,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - 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'], + 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'], }) export class IonSelect { protected el: HTMLIonSelectElement; @@ -2231,14 +2231,14 @@ This event will not emit when programmatically setting the `value` property. @ProxyCmp({ - inputs: ['cancelText', 'header', 'multiple', 'options'] + inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'] }) @Component({ selector: 'ion-select-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelText', 'header', 'multiple', 'options'], + inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'], }) export class IonSelectModal { protected el: HTMLIonSelectModalElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 41e27889758..be04cac9ee0 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -1962,14 +1962,14 @@ export declare interface IonSegmentView extends Components.IonSegmentView { @ProxyCmp({ defineCustomElementFn: defineIonSelectModal, - inputs: ['cancelText', 'header', 'multiple', 'options'] + inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'] }) @Component({ selector: 'ion-select-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelText', 'header', 'multiple', 'options'], + inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'], standalone: true }) export class IonSelectModal { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index bf7ab19881d..9fba6dc4254 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -949,6 +949,7 @@ export const IonSegmentView: StencilVueComponent = /*@__PURE export const IonSelect: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-select', defineIonSelect, [ 'cancelText', + 'cancelIcon', 'color', 'compareWith', 'disabled', @@ -991,6 +992,7 @@ export const IonSelect: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-select-modal', defineIonSelectModal, [ 'header', 'cancelText', + 'cancelIcon', 'multiple', 'options' ]);