/*!
 * (C) Ionic http://ionicframework.com - MIT License
 */
import checkRegular from "@phosphor-icons/core/assets/regular/check.svg";
import minusRegular from "@phosphor-icons/core/assets/regular/minus.svg";
import { Build, Host, h } from "@stencil/core";
import { checkInvalidState } from "../../utils/forms/index";
import { renderHiddenInput, inheritAriaAttributes } from "../../utils/helpers";
import { hapticSelection } from "../../utils/native/haptic";
import { isPlatform } from "../../utils/platform";
import { isRTL } from "../../utils/rtl/index";
import { createColorClasses, hostContext } from "../../utils/theme";
import { checkmarkOutline, removeOutline, ellipseOutline } from "ionicons/icons";
import { config } from "../../global/config";
import { getIonTheme } from "../../global/ionic-global";
/**
 * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
 * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
 *
 * @slot - The label text to associate with the toggle. Use the "labelPlacement" property to control where the label is placed relative to the toggle.
 *
 * @part track - The background track of the toggle.
 * @part handle - The toggle handle, or knob, used to change the checked state.
 * @part label - The label text describing the toggle.
 * @part supporting-text - Supporting text displayed beneath the toggle label.
 * @part helper-text - Supporting text displayed beneath the toggle label when the toggle is valid.
 * @part error-text - Supporting text displayed beneath the toggle label when the toggle is invalid and touched.
 */
export class Toggle {
    constructor() {
        this.inputId = `ion-tg-${toggleIds++}`;
        this.inputLabelId = `${this.inputId}-lbl`;
        this.helperTextId = `${this.inputId}-helper-text`;
        this.errorTextId = `${this.inputId}-error-text`;
        this.lastDrag = 0;
        this.inheritedAttributes = {};
        this.didLoad = false;
        this.activated = false;
        /**
         * Track validation state for proper aria-live announcements.
         */
        this.isInvalid = false;
        /**
         * The name of the control, which is submitted with the form data.
         */
        this.name = this.inputId;
        /**
         * If `true`, the toggle is selected.
         */
        this.checked = false;
        /**
         * If `true`, the user cannot interact with the toggle.
         */
        this.disabled = false;
        /**
         * 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 `<input type="checkbox">`,
         * it's only used when the toggle participates in a native `<form>`.
         */
        this.value = 'on';
        /**
         * Enables the on/off accessibility switch labels within the toggle.
         */
        this.enableOnOffLabels = config.get('toggleOnOffLabels');
        /**
         * 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.
         * `"end"`: The label will appear to the right of the toggle in LTR and to the left in RTL.
         * `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
         * `"stacked"`: The label will appear above the toggle regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
         */
        this.labelPlacement = 'start';
        /**
         * If true, screen readers will announce it as a required field. This property
         * works only for accessibility purposes, it will not prevent the form from
         * submitting if the value is invalid.
         */
        this.required = false;
        this.setupGesture = async () => {
            const { toggleTrack } = this;
            if (toggleTrack) {
                this.gesture = (await import('../../utils/gesture')).createGesture({
                    el: toggleTrack,
                    gestureName: 'toggle',
                    gesturePriority: 100,
                    threshold: 5,
                    passive: false,
                    onStart: () => this.onStart(),
                    onMove: (ev) => this.onMove(ev),
                    onEnd: (ev) => this.onEnd(ev),
                });
                this.disabledChanged();
            }
        };
        this.onKeyDown = (ev) => {
            if (ev.key === ' ') {
                ev.preventDefault();
                if (!this.disabled) {
                    this.toggleChecked();
                }
            }
        };
        this.onClick = (ev) => {
            /**
             * The haptics for the toggle on tap is
             * an iOS-only feature. As such, it should
             * only trigger on iOS.
             */
            const enableHaptics = isPlatform('ios');
            if (this.disabled) {
                return;
            }
            ev.preventDefault();
            if (this.lastDrag + 300 < Date.now()) {
                this.toggleChecked();
                enableHaptics && hapticSelection();
            }
        };
        /**
         * Stops propagation when the display label is clicked,
         * otherwise, two clicks will be triggered.
         */
        this.onDivLabelClick = (ev) => {
            ev.stopPropagation();
        };
        this.onFocus = () => {
            this.ionFocus.emit();
        };
        this.onBlur = () => {
            this.ionBlur.emit();
        };
        this.getSwitchLabelIcon = (checked) => {
            return checked ? this.toggleCheckedIcon : this.toggleUncheckedIcon;
        };
    }
    disabledChanged() {
        if (this.gesture) {
            this.gesture.enable(!this.disabled);
        }
    }
    toggleChecked() {
        const { checked, value } = this;
        const isNowChecked = !checked;
        this.checked = isNowChecked;
        this.ionChange.emit({
            checked: isNowChecked,
            value,
        });
    }
    async connectedCallback() {
        const { didLoad, el } = this;
        /**
         * If we have not yet rendered
         * ion-toggle, then toggleTrack is not defined.
         * But if we are moving ion-toggle via appendChild,
         * then toggleTrack will be defined.
         */
        if (didLoad) {
            this.setupGesture();
        }
        // Watch for class changes to update validation state.
        if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
            this.validationObserver = new MutationObserver(() => {
                const newIsInvalid = checkInvalidState(el);
                if (this.isInvalid !== newIsInvalid) {
                    this.isInvalid = newIsInvalid;
                    /**
                     * Screen readers tend to announce changes
                     * to `aria-describedby` when the attribute
                     * is changed during a blur event for a
                     * native form control.
                     * However, the announcement can be spotty
                     * when using a non-native form control
                     * and `forceUpdate()`.
                     * This is due to `forceUpdate()` internally
                     * rescheduling the DOM update to a lower
                     * priority queue regardless if it's called
                     * inside a Promise or not, thus causing
                     * the screen reader to potentially miss the
                     * change.
                     * By using a State variable inside a Promise,
                     * it guarantees a re-render immediately at
                     * a higher priority.
                     */
                    Promise.resolve().then(() => {
                        this.hintTextId = this.getHintTextId();
                    });
                }
            });
            this.validationObserver.observe(el, {
                attributes: true,
                attributeFilter: ['class'],
            });
        }
        // Always set initial state
        this.isInvalid = checkInvalidState(el);
    }
    componentDidLoad() {
        this.setupGesture();
        this.didLoad = true;
    }
    disconnectedCallback() {
        if (this.gesture) {
            this.gesture.destroy();
            this.gesture = undefined;
        }
        // Clean up validation observer to prevent memory leaks.
        if (this.validationObserver) {
            this.validationObserver.disconnect();
            this.validationObserver = undefined;
        }
    }
    componentWillLoad() {
        this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
        this.hintTextId = this.getHintTextId();
    }
    onStart() {
        this.activated = true;
        // touch-action does not work in iOS
        this.setFocus();
    }
    onMove(detail) {
        if (shouldToggle(isRTL(this.el), this.checked, detail.deltaX, -10)) {
            this.toggleChecked();
            hapticSelection();
        }
    }
    onEnd(ev) {
        this.activated = false;
        this.lastDrag = Date.now();
        ev.event.preventDefault();
        ev.event.stopImmediatePropagation();
    }
    getValue() {
        return this.value || '';
    }
    setFocus() {
        this.el.focus();
    }
    get toggleDefaultCheckedIcon() {
        // Determine the theme and map to default icons
        const theme = getIonTheme(this);
        const defaultIcons = {
            ios: removeOutline,
            ionic: checkRegular,
            md: checkmarkOutline,
        };
        // Get the default icon based on the theme, falling back to 'md' icon if necessary
        const defaultIcon = defaultIcons[theme] || defaultIcons.md;
        // Return the default icon
        return defaultIcon;
    }
    /**
     * Get the icon to use for the checked icon.
     * Otherwise, use the icon set in the config.
     * If no icon is set in the config, use the default icon.
     */
    get toggleCheckedIcon() {
        return config.get('toggleCheckedIcon', this.toggleDefaultCheckedIcon);
    }
    /**
     * Get the icon to use for the unchecked icon.
     * Otherwise, use the icon set in the config.
     * If no icon is set in the config, use the default icon.
     */
    get toggleUncheckedIcon() {
        // Determine the theme and map to default icons
        const theme = getIonTheme(this);
        const defaultIcons = {
            ios: ellipseOutline,
            ionic: minusRegular,
            md: removeOutline,
        };
        // Get the default icon based on the theme, falling back to 'md' icon if necessary
        const defaultIcon = defaultIcons[theme] || defaultIcons.md;
        // Return the configured toggle unchecked icon or the default icon
        return config.get('toggleUncheckedIcon', defaultIcon);
    }
    renderOnOffSwitchLabels(checked) {
        const icon = this.getSwitchLabelIcon(checked);
        return (h("ion-icon", { class: {
                'toggle-switch-icon': true,
                'toggle-switch-icon-checked': checked,
                /**
                 * The default checked icon is being modified with
                 * styling that causes it to rotate by 90 degrees
                 * when the theme is `ios`.
                 *
                 * To prevent any rotation on a custom icon that is
                 * set through the config, we need to apply a class
                 * that handles the styling only when the default
                 * checked icon is being used.
                 */
                'toggle-switch-icon-checked-default': checked && icon === this.toggleDefaultCheckedIcon,
            }, icon: icon, "aria-hidden": "true" }));
    }
    renderToggleControl() {
        const theme = getIonTheme(this);
        const { enableOnOffLabels, checked } = this;
        return (h("div", { class: "toggle-icon", part: "track", ref: (el) => (this.toggleTrack = el) }, enableOnOffLabels &&
            theme === 'ios' && [this.renderOnOffSwitchLabels(true), this.renderOnOffSwitchLabels(false)], h("div", { class: "toggle-icon-wrapper" }, h("div", { class: "toggle-inner", part: "handle" }, enableOnOffLabels && (theme === 'md' || theme === 'ionic') && this.renderOnOffSwitchLabels(checked)))));
    }
    get hasLabel() {
        return this.el.textContent !== '';
    }
    getHintTextId() {
        const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
        if (isInvalid && errorText) {
            return errorTextId;
        }
        if (helperText) {
            return helperTextId;
        }
        return undefined;
    }
    /**
     * Responsible for rendering helper text and error text.
     * This element should only be rendered if hint text is set.
     */
    renderHintText() {
        const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
        /**
         * undefined and empty string values should
         * be treated as not having helper/error text.
         */
        const hasHintText = !!helperText || !!errorText;
        if (!hasHintText) {
            return;
        }
        return (h("div", { class: "toggle-bottom" }, h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null), h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null)));
    }
    render() {
        const { activated, alignment, checked, color, disabled, el, hasLabel, inheritedAttributes, inputId, inputLabelId, justify, labelPlacement, name, required, } = this;
        const theme = getIonTheme(this);
        const value = this.getValue();
        const rtl = isRTL(el) ? 'rtl' : 'ltr';
        const isIonicTheme = theme === 'ionic';
        renderHiddenInput(true, el, name, checked ? value : '', disabled);
        return (h(Host, { key: 'b12951146a79d3839e6263e91e747398a0fa901d', role: "switch", "aria-checked": `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, "aria-labelledby": hasLabel ? inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, class: createColorClasses(color, {
                [theme]: true,
                'in-item': hostContext('ion-item', el),
                'toggle-activated': activated,
                'toggle-checked': checked,
                'toggle-disabled': disabled,
                [`toggle-justify-${justify}`]: justify !== undefined,
                [`toggle-alignment-${alignment}`]: alignment !== undefined,
                [`toggle-label-placement-${labelPlacement}`]: true,
                [`toggle-${rtl}`]: true,
                'ion-activatable': isIonicTheme,
                'ion-focusable': isIonicTheme,
            }) }, h("label", { key: 'cf1d27243bf9bdd5bafaf6d86b76beb311b5e01c', class: "toggle-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: 'b3eb0f146836f5994c15bf1fd67a79f72b9414f7', type: "checkbox", role: "switch", "aria-checked": `${checked}`, checked: checked, disabled: disabled, id: inputId, required: required }, inheritedAttributes)), h("div", { key: '56ddcfd6452f30fd6b38d0fd85be3ec966aa120b', class: {
                'label-text-wrapper': true,
                'label-text-wrapper-hidden': !hasLabel,
            }, part: "label", id: inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '706e5b667ee242fb1afb3e4afa613f9798fbe25b' }), this.renderHintText()), h("div", { key: '255ccd7d60f7d78b4eba8d380b303a72093e9d16', class: "native-wrapper" }, this.renderToggleControl()))));
    }
    static get is() { return "ion-toggle"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "ios": ["toggle.ios.scss"],
            "md": ["toggle.md.scss"],
            "ionic": ["toggle.ionic.scss"]
        };
    }
    static get styleUrls() {
        return {
            "ios": ["toggle.ios.css"],
            "md": ["toggle.md.css"],
            "ionic": ["toggle.ionic.css"]
        };
    }
    static get properties() {
        return {
            "color": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "Color",
                    "resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined",
                    "references": {
                        "Color": {
                            "location": "import",
                            "path": "../../interface",
                            "id": "src/interface.d.ts::Color"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "color"
            },
            "name": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The name of the control, which is submitted with the form data."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "name",
                "defaultValue": "this.inputId"
            },
            "checked": {
                "type": "boolean",
                "mutable": true,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the toggle is selected."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "checked",
                "defaultValue": "false"
            },
            "disabled": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the user cannot interact with the toggle."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "disabled",
                "defaultValue": "false"
            },
            "errorText": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Text that is placed under the toggle label and displayed when an error is detected."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "error-text"
            },
            "helperText": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Text that is placed under the toggle label and displayed when no error is detected."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "helper-text"
            },
            "value": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string | null",
                    "resolved": "null | string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The value of the toggle does not mean if it's checked or not, use the `checked`\nproperty for that.\n\nThe value of a toggle is analogous to the value of a `<input type=\"checkbox\">`,\nit's only used when the toggle participates in a native `<form>`."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "value",
                "defaultValue": "'on'"
            },
            "enableOnOffLabels": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean | undefined",
                    "resolved": "boolean | undefined",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Enables the on/off accessibility switch labels within the toggle."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "enable-on-off-labels",
                "defaultValue": "config.get('toggleOnOffLabels')"
            },
            "labelPlacement": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "'start' | 'end' | 'fixed' | 'stacked'",
                    "resolved": "\"end\" | \"fixed\" | \"stacked\" | \"start\"",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Where to place the label relative to the input.\n`\"start\"`: The label will appear to the left of the toggle in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the toggle in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\").\n`\"stacked\"`: The label will appear above the toggle regardless of the direction. The alignment of the label can be controlled with the `alignment` property."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "label-placement",
                "defaultValue": "'start'"
            },
            "justify": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "'start' | 'end' | 'space-between'",
                    "resolved": "\"end\" | \"space-between\" | \"start\" | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "How to pack the label and toggle within a line.\n`\"start\"`: The label and toggle will appear on the left in LTR and\non the right in RTL.\n`\"end\"`: The label and toggle will appear on the right in LTR and\non the left in RTL.\n`\"space-between\"`: The label and toggle will appear on opposite\nends of the line with space between the two elements.\nSetting this property will change the toggle `display` to `block`."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "justify"
            },
            "alignment": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "'start' | 'center'",
                    "resolved": "\"center\" | \"start\" | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "How to control the alignment of the toggle and label on the cross axis.\n`\"start\"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.\n`\"center\"`: The label and control will appear at the center of the cross axis in both LTR and RTL.\nSetting this property will change the toggle `display` to `block`."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "alignment"
            },
            "required": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If true, screen readers will announce it as a required field. This property\nworks only for accessibility purposes, it will not prevent the form from\nsubmitting if the value is invalid."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "required",
                "defaultValue": "false"
            }
        };
    }
    static get states() {
        return {
            "activated": {},
            "isInvalid": {},
            "hintTextId": {}
        };
    }
    static get events() {
        return [{
                "method": "ionChange",
                "name": "ionChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the user switches the toggle on or off.\n\nThis event will not emit when programmatically setting the `checked` property."
                },
                "complexType": {
                    "original": "ToggleChangeEventDetail",
                    "resolved": "ToggleChangeEventDetail<any>",
                    "references": {
                        "ToggleChangeEventDetail": {
                            "location": "import",
                            "path": "./toggle-interface",
                            "id": "src/components/toggle/toggle-interface.ts::ToggleChangeEventDetail"
                        }
                    }
                }
            }, {
                "method": "ionFocus",
                "name": "ionFocus",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the toggle has focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }, {
                "method": "ionBlur",
                "name": "ionBlur",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the toggle loses focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }];
    }
    static get elementRef() { return "el"; }
    static get watchers() {
        return [{
                "propName": "disabled",
                "methodName": "disabledChanged"
            }];
    }
}
const shouldToggle = (rtl, checked, deltaX, margin) => {
    if (checked) {
        return (!rtl && margin > deltaX) || (rtl && -margin < deltaX);
    }
    else {
        return (!rtl && -margin < deltaX) || (rtl && margin > deltaX);
    }
};
let toggleIds = 0;
