From 3a57650ffedc00b1ef0031494398548c12ae2ffc Mon Sep 17 00:00:00 2001 From: AbdullahAhmadAAK Date: Thu, 19 Mar 2026 06:47:39 +0500 Subject: [PATCH] feat(toggle): add checkedIcon and uncheckedIcon properties for custom icons --- core/api.txt | 2 + core/src/components.d.ts | 18 +++++++ .../components/toggle/test/states/index.html | 6 ++- .../src/components/toggle/test/toggle.spec.ts | 53 +++++++++++++++++++ core/src/components/toggle/toggle.md.scss | 14 +++-- core/src/components/toggle/toggle.tsx | 19 ++++++- core/src/utils/config.ts | 10 ++++ packages/angular/src/directives/proxies.ts | 4 +- packages/vue/src/proxies.ts | 2 + 9 files changed, 119 insertions(+), 9 deletions(-) diff --git a/core/api.txt b/core/api.txt index d9e586ffd33..e6b63f50409 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2068,6 +2068,7 @@ ion-toast,part,wrapper ion-toggle,shadow ion-toggle,prop,alignment,"center" | "start" | undefined,undefined,false,false ion-toggle,prop,checked,boolean,false,false,false +ion-toggle,prop,checkedIcon,null | string | undefined,undefined,false,false ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-toggle,prop,disabled,boolean,false,false,false ion-toggle,prop,enableOnOffLabels,boolean | undefined,config.get('toggleOnOffLabels'),false,false @@ -2078,6 +2079,7 @@ ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',fal ion-toggle,prop,mode,"ios" | "md",undefined,false,false ion-toggle,prop,name,string,this.inputId,false,false ion-toggle,prop,required,boolean,false,false,false +ion-toggle,prop,uncheckedIcon,null | string | undefined,undefined,false,false ion-toggle,prop,value,null | string | undefined,'on',false,false ion-toggle,event,ionBlur,void,true ion-toggle,event,ionChange,ToggleChangeEventDetail,true diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 37223c4f77a..97885854a24 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3700,6 +3700,10 @@ export namespace Components { * @default false */ "checked": boolean; + /** + * The built-in named SVG icon name or the exact `src` of an SVG file to use when the toggle is checked. + */ + "checkedIcon"?: string | null; /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ @@ -3745,6 +3749,10 @@ export namespace Components { * @default false */ "required": boolean; + /** + * The built-in named SVG icon name or the exact `src` of an SVG file to use when the toggle is unchecked. + */ + "uncheckedIcon"?: string | null; /** * The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a ``, it's only used when the toggle participates in a native `
`. * @default 'on' @@ -9094,6 +9102,10 @@ declare namespace LocalJSX { * @default false */ "checked"?: boolean; + /** + * The built-in named SVG icon name or the exact `src` of an SVG file to use when the toggle is checked. + */ + "checkedIcon"?: string | null; /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ @@ -9151,6 +9163,10 @@ declare namespace LocalJSX { * @default false */ "required"?: boolean; + /** + * The built-in named SVG icon name or the exact `src` of an SVG file to use when the toggle is unchecked. + */ + "uncheckedIcon"?: string | null; /** * The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a ``, it's only used when the toggle participates in a native ``. * @default 'on' @@ -9907,6 +9923,8 @@ declare namespace LocalJSX { "helperText": string; "value": string | null; "enableOnOffLabels": boolean | undefined; + "checkedIcon": string | null; + "uncheckedIcon": string | null; "labelPlacement": 'start' | 'end' | 'fixed' | 'stacked'; "justify": 'start' | 'end' | 'space-between'; "alignment": 'start' | 'center'; diff --git a/core/src/components/toggle/test/states/index.html b/core/src/components/toggle/test/states/index.html index a1cc5cf78cf..69b8675c983 100644 --- a/core/src/components/toggle/test/states/index.html +++ b/core/src/components/toggle/test/states/index.html @@ -52,7 +52,7 @@

Unchecked

- Enable Notifications + Enable Notifications
@@ -60,6 +60,10 @@

Checked

Enable Notifications
+ Icons Toggle + + +

Disabled, Unchecked

Enable Notifications diff --git a/core/src/components/toggle/test/toggle.spec.ts b/core/src/components/toggle/test/toggle.spec.ts index 8ba55eb70ba..93418b2b5e9 100644 --- a/core/src/components/toggle/test/toggle.spec.ts +++ b/core/src/components/toggle/test/toggle.spec.ts @@ -1,4 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; +import { checkmarkOutline, ellipseOutline, removeOutline } from 'ionicons/icons'; import { config } from '../../../global/config'; import { Toggle } from '../toggle'; @@ -41,6 +42,58 @@ describe('toggle', () => { }); }); + describe('checkedIcon and uncheckedIcon', () => { + it('should set custom checked icon on the instance, overriding config', async () => { + const t = await newToggle(); + t.checkedIcon = 'custom-checked-icon-instance'; + config.reset({ + toggleCheckedIcon: 'custom-checked-icon-config', + }); + + expect((t as any).getSwitchLabelIcon('md', true)).toBe('custom-checked-icon-instance'); + }); + + it('should set custom unchecked icon on the instance, overriding config', async () => { + const t = await newToggle(); + t.uncheckedIcon = 'custom-unchecked-icon-instance'; + config.reset({ + toggleUncheckedIcon: 'custom-unchecked-icon-config', + }); + + expect((t as any).getSwitchLabelIcon('md', false)).toBe('custom-unchecked-icon-instance'); + }); + + it('should set custom checked icon in the config', async () => { + const t = await newToggle(); + config.reset({ + toggleCheckedIcon: 'custom-checked-icon-config', + }); + + expect((t as any).getSwitchLabelIcon('md', true)).toBe('custom-checked-icon-config'); + }); + + it('should set custom unchecked icon in the config', async () => { + const t = await newToggle(); + config.reset({ + toggleUncheckedIcon: 'custom-unchecked-icon-config', + }); + + expect((t as any).getSwitchLabelIcon('md', false)).toBe('custom-unchecked-icon-config'); + }); + + it('should use default icons in md mode', async () => { + const t = await newToggle(); + expect((t as any).getSwitchLabelIcon('md', true)).toBe(checkmarkOutline); + expect((t as any).getSwitchLabelIcon('md', false)).toBe(removeOutline); + }); + + it('should use default icons in ios mode', async () => { + const t = await newToggle(); + expect((t as any).getSwitchLabelIcon('ios', true)).toBe(removeOutline); + expect((t as any).getSwitchLabelIcon('ios', false)).toBe(ellipseOutline); + }); + }); + describe('shadow parts', () => { it('should have shadow parts', async () => { const page = await newSpecPage({ diff --git a/core/src/components/toggle/toggle.md.scss b/core/src/components/toggle/toggle.md.scss index d0a3bfda360..0ed652a66f6 100644 --- a/core/src/components/toggle/toggle.md.scss +++ b/core/src/components/toggle/toggle.md.scss @@ -17,6 +17,9 @@ --handle-max-height: #{$toggle-md-handle-max-height}; --handle-spacing: 0; --handle-transition: #{$toggle-md-transition}; + --on-off-label-icon-size: 13px; + + } // Toggle Native Wrapper @@ -61,10 +64,13 @@ } .toggle-inner .toggle-switch-icon { - @include padding(1px); - - width: 100%; - height: 100%; +width: var(--on-off-label-icon-size); +height: var(--on-off-label-icon-size); +min-width: var(--on-off-label-icon-size); +min-height: var(--on-off-label-icon-size); +display: block; +opacity: 0.86; +transition: transform $toggle-md-transition-duration, opacity $toggle-md-transition-duration, color $toggle-md-transition-duration; } // Material Design Toggle: Disabled diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index b46635c8a78..3a177ffe049 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -104,6 +104,18 @@ export class Toggle implements ComponentInterface { */ @Prop() enableOnOffLabels: boolean | undefined = config.get('toggleOnOffLabels'); + /** + * The built-in named SVG icon name or the exact `src` of an SVG file + * to use when the toggle is checked. + */ + @Prop() checkedIcon?: string | null; + + /** + * The built-in named SVG icon name or the exact `src` of an SVG file + * to use when the toggle is unchecked. + */ + @Prop() uncheckedIcon?: string | null; + /** * Where to place the label relative to the input. * `"start"`: The label will appear to the left of the toggle in LTR and to the right in RTL. @@ -348,10 +360,13 @@ export class Toggle implements ComponentInterface { }; private getSwitchLabelIcon = (mode: Mode, checked: boolean) => { + const checkedIcon = this.checkedIcon ?? config.get('toggleCheckedIcon'); + const uncheckedIcon = this.uncheckedIcon ?? config.get('toggleUncheckedIcon'); + if (mode === 'md') { - return checked ? checkmarkOutline : removeOutline; + return checked ? checkedIcon ?? checkmarkOutline : uncheckedIcon ?? removeOutline; } - return checked ? removeOutline : ellipseOutline; + return checked ? checkedIcon ?? removeOutline : uncheckedIcon ?? ellipseOutline; }; private renderOnOffSwitchLabels(mode: Mode, checked: boolean) { diff --git a/core/src/utils/config.ts b/core/src/utils/config.ts index 4b679395efa..07c0d2728d6 100644 --- a/core/src/utils/config.ts +++ b/core/src/utils/config.ts @@ -72,6 +72,16 @@ export interface IonicConfig { */ toggleOnOffLabels?: boolean; + /** + * Overrides the default checked icon in all `` components. + */ + toggleCheckedIcon?: string; + + /** + * Overrides the default unchecked icon in all `` components. + */ + toggleUncheckedIcon?: string; + /** * Overrides the default spinner for all `ion-loading` overlays, ie. the ones * created with `ion-loading-controller`. diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 1c78b120d9d..64e7ae625e0 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2567,14 +2567,14 @@ Shorthand for ionToastDidDismiss. @ProxyCmp({ - inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'] + inputs: ['alignment', 'checked', 'checkedIcon', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'uncheckedIcon', 'value'] }) @Component({ selector: 'ion-toggle', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'], + inputs: ['alignment', 'checked', 'checkedIcon', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'uncheckedIcon', 'value'], }) export class IonToggle { protected el: HTMLIonToggleElement; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 735a7906974..f186e836f5a 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -1066,6 +1066,8 @@ export const IonToggle: StencilVueComponent