UNPKG

@ionic/core

Version:
1,409 lines • 59.8 kB
/*!
 * (C) Ionic http://ionicframework.com - MIT License
 */
import { Build, Host, h } from "@stencil/core";
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from "../../utils/content/index";
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from "../../utils/helpers";
import { printIonWarning } from "../../utils/logging/index";
import { isRTL } from "../../utils/rtl/index";
import { createColorClasses, hostContext } from "../../utils/theme";
import { getIonMode } from "../../global/ionic-global";
import { roundToMaxDecimalPlaces } from "../../utils/floating-point";
// TODO(FW-2832): types
/**
 * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
 *
 * @slot label - The label text to associate with the range. Use the "labelPlacement" property to control where the label is placed relative to the range.
 * @slot start - Content is placed to the left of the range slider in LTR, and to the right in RTL.
 * @slot end - Content is placed to the right of the range slider in LTR, and to the left in RTL.
 *
 * @part label - The label text describing the range.
 * @part tick - An inactive tick mark.
 * @part tick-active - An active tick mark.
 * @part bar - The inactive part of the bar.
 * @part bar-active - The active part of the bar.
 * @part knob-handle - The container that wraps the knob and handles drag interactions.
 * @part knob-handle-a - The container for the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part knob-handle-b - The container for the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part knob-handle-lower - The container for the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross.
 * @part knob-handle-upper - The container for the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob handle they refer to when the knobs cross.
 * @part pin - The value indicator displayed above a knob.
 * @part pin-a - The value indicator above the knob with the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part pin-b - The value indicator above the knob with the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part pin-lower - The value indicator above the knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross.
 * @part pin-upper - The value indicator above the knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which pin they refer to when the knobs cross.
 * @part knob - The visual knob element on the range track.
 * @part knob-a - The visual knob for the static `A` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part knob-b - The visual knob for the static `B` identity when `dualKnobs` is `true`. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
 * @part knob-lower - The visual knob whose current `value` is `lower` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross.
 * @part knob-upper - The visual knob whose current `value` is `upper` when `dualKnobs` is `true`. The lower and upper parts swap which knob they refer to when the knobs cross.
 * @part activated - Added to the knob-handle, knob, and pin when the knob is active. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part focused - Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part hover - Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pressed - Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when `dualKnobs` is `true`.
 */
export class Range {
    constructor() {
        this.rangeId = `ion-r-${rangeIds++}`;
        this.didLoad = false;
        this.noUpdate = false;
        this.hasFocus = false;
        this.inheritedAttributes = {};
        this.contentEl = null;
        this.initialContentScrollY = true;
        /**
         * Used to avoid setting the focused state on click or tap. The focused
         * state is only set when the focus comes from the keyboard (e.g. Tab).
         * This is set to true on pointer down (mouse/touch).
         */
        this.focusFromPointer = false;
        this.ratioA = 0;
        this.ratioB = 0;
        /**
         * The name of the control, which is submitted with the form data.
         */
        this.name = this.rangeId;
        /**
         * If `true`, the range shows two knobs and `value` is an object with `lower`
         * and `upper` properties. If `false`, the range shows one knob and `value` is
         * a number.
         */
        this.dualKnobs = false;
        /**
         * Minimum integer value of the range.
         */
        this.min = 0;
        /**
         * Maximum integer value of the range.
         */
        this.max = 100;
        /**
         * If `true`, a pin with integer value is shown when the knob
         * is pressed.
         */
        this.pin = false;
        /**
         * A callback used to format the pin text.
         * By default the pin text is set to `Math.round(value)`.
         *
         * See https://ionicframework.com/docs/troubleshooting/runtime#accessing-this
         * if you need to access `this` from within the callback.
         */
        this.pinFormatter = (value) => Math.round(value);
        /**
         * If `true`, the knob snaps to tick marks evenly spaced based
         * on the step property value.
         */
        this.snaps = false;
        /**
         * Specifies the value granularity.
         */
        this.step = 1;
        /**
         * If `true`, tick marks are displayed based on the step value.
         * Only applies when `snaps` is `true`.
         */
        this.ticks = true;
        /**
         * If `true`, the user cannot interact with the range.
         */
        this.disabled = false;
        /**
         * the value of the range.
         */
        this.value = 0;
        /**
         * Compares two RangeValue inputs to determine if they are different.
         *
         * @param newVal - The new value.
         * @param oldVal - The old value.
         * @returns `true` if the values are different, `false` otherwise.
         */
        this.compareValues = (newVal, oldVal) => {
            if (typeof newVal === 'object' && typeof oldVal === 'object') {
                return newVal.lower !== oldVal.lower || newVal.upper !== oldVal.upper;
            }
            return newVal !== oldVal;
        };
        this.clampBounds = (value) => {
            return clamp(this.min, value, this.max);
        };
        this.ensureValueInBounds = (value) => {
            if (this.dualKnobs) {
                return {
                    lower: this.clampBounds(value.lower),
                    upper: this.clampBounds(value.upper),
                };
            }
            else {
                return this.clampBounds(value);
            }
        };
        /**
         * Where to place the label relative to the range.
         * `"start"`: The label will appear to the left of the range in LTR and to the right in RTL.
         * `"end"`: The label will appear to the right of the range 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 range regardless of the direction.
         */
        this.labelPlacement = 'start';
        this.setupGesture = async () => {
            const rangeSlider = this.rangeSlider;
            if (rangeSlider) {
                this.gesture = (await import('../../utils/gesture')).createGesture({
                    el: rangeSlider,
                    gestureName: 'range',
                    gesturePriority: 100,
                    /**
                     * Provide a threshold since the drag movement
                     * might be a user scrolling the view.
                     * If this is true, then the range
                     * should not move.
                     */
                    threshold: 10,
                    onStart: () => this.onStart(),
                    onMove: (ev) => this.onMove(ev),
                    onEnd: (ev) => this.onEnd(ev),
                });
                this.gesture.enable(!this.disabled);
            }
        };
        /**
         * Observes the knob handles for the ion-activated class and syncs
         * activatedKnob so the activated part is correctly set on the handle,
         * knob, and pin.
         */
        this.setupActivatedObserver = () => {
            const knobHandleA = this.el.shadowRoot.querySelector('.range-knob-handle-a');
            const knobHandleB = this.el.shadowRoot.querySelector('.range-knob-handle-b');
            const syncActivated = () => {
                this.activatedKnob = knobHandleA?.classList.contains('ion-activated')
                    ? 'A'
                    : knobHandleB?.classList.contains('ion-activated')
                        ? 'B'
                        : undefined;
            };
            if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
                this.activatedObserver = new MutationObserver(syncActivated);
                this.activatedObserver.observe(this.el.shadowRoot, {
                    attributes: true,
                    attributeFilter: ['class'],
                    subtree: true,
                });
            }
            syncActivated();
        };
        this.handleKeyboard = (knob, isIncrease) => {
            const { ensureValueInBounds } = this;
            let step = this.step;
            step = step > 0 ? step : 1;
            step = step / (this.max - this.min);
            if (!isIncrease) {
                step *= -1;
            }
            if (knob === 'A') {
                this.ratioA = clamp(0, this.ratioA + step, 1);
            }
            else {
                this.ratioB = clamp(0, this.ratioB + step, 1);
            }
            this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
            this.updateValue();
            this.emitValueChange();
            this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
        };
        this.onBlur = () => {
            if (this.hasFocus) {
                this.hasFocus = false;
                this.focusedKnob = undefined;
                this.ionBlur.emit();
            }
        };
        this.onFocus = () => {
            if (!this.hasFocus) {
                this.hasFocus = true;
                this.ionFocus.emit();
            }
        };
        this.onKnobFocus = (knob) => {
            // Clicking focuses the range which is needed for the keyboard,
            // but we only want to add the ion-focused class when focused via Tab.
            if (!this.focusFromPointer) {
                this.focusedKnob = knob;
            }
            else {
                this.focusFromPointer = false;
                this.focusedKnob = undefined;
            }
            // If the knob was not already focused, emit the focus event
            if (!this.hasFocus) {
                this.hasFocus = true;
                this.ionFocus.emit();
            }
        };
        this.onKnobBlur = () => {
            // Check if focus is moving to another knob within the same range
            // by delaying the reset to allow the new focus to register
            setTimeout(() => {
                const activeElement = this.el.shadowRoot?.activeElement;
                const isStillFocusedOnKnob = activeElement && activeElement.classList.contains('range-knob-handle');
                if (!isStillFocusedOnKnob) {
                    if (this.hasFocus) {
                        this.hasFocus = false;
                        this.focusedKnob = undefined;
                        this.ionBlur.emit();
                    }
                }
            }, 0);
        };
        this.onKnobMouseEnter = (knob) => {
            this.hoveredKnob = knob;
        };
        this.onKnobMouseLeave = () => {
            this.hoveredKnob = undefined;
        };
    }
    debounceChanged() {
        const { ionInput, debounce, originalIonInput } = this;
        /**
         * If debounce is undefined, we have to manually revert the ionInput emitter in case
         * debounce used to be set to a number. Otherwise, the event would stay debounced.
         */
        this.ionInput = debounce === undefined ? originalIonInput ?? ionInput : debounceEvent(ionInput, debounce);
    }
    dualKnobsChanged() {
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    minChanged(newValue) {
        if (!isSafeNumber(newValue)) {
            this.min = 0;
        }
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    maxChanged(newValue) {
        if (!isSafeNumber(newValue)) {
            this.max = 100;
        }
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    stepChanged(newValue) {
        if (!isSafeNumber(newValue)) {
            this.step = 1;
        }
    }
    activeBarStartChanged() {
        const { activeBarStart } = this;
        if (activeBarStart !== undefined) {
            if (activeBarStart > this.max) {
                printIonWarning(`[ion-range] - The value of activeBarStart (${activeBarStart}) is greater than the max (${this.max}). Valid values are greater than or equal to the min value and less than or equal to the max value.`, this.el);
                this.activeBarStart = this.max;
            }
            else if (activeBarStart < this.min) {
                printIonWarning(`[ion-range] - The value of activeBarStart (${activeBarStart}) is less than the min (${this.min}). Valid values are greater than or equal to the min value and less than or equal to the max value.`, this.el);
                this.activeBarStart = this.min;
            }
        }
    }
    disabledChanged() {
        if (this.gesture) {
            this.gesture.enable(!this.disabled);
        }
    }
    valueChanged(newValue, oldValue) {
        const valuesChanged = this.compareValues(newValue, oldValue);
        if (valuesChanged) {
            this.ionInput.emit({ value: this.value });
        }
        if (!this.noUpdate) {
            this.updateRatio();
        }
    }
    componentWillLoad() {
        /**
         * If user has custom ID set then we should
         * not assign the default incrementing ID.
         */
        if (this.el.hasAttribute('id')) {
            this.rangeId = this.el.getAttribute('id');
        }
        this.inheritedAttributes = inheritAriaAttributes(this.el);
        // If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
        // Each watch does this, but not before the initial load.
        this.min = isSafeNumber(this.min) ? this.min : 0;
        this.max = isSafeNumber(this.max) ? this.max : 100;
        this.step = isSafeNumber(this.step) ? this.step : 1;
    }
    componentDidLoad() {
        this.originalIonInput = this.ionInput;
        this.setupGesture();
        this.updateRatio();
        this.setupActivatedObserver();
        this.didLoad = true;
    }
    connectedCallback() {
        this.updateRatio();
        this.debounceChanged();
        this.disabledChanged();
        this.activeBarStartChanged();
        /**
         * If we have not yet rendered
         * ion-range, then rangeSlider is not defined.
         * But if we are moving ion-range via appendChild,
         * then rangeSlider will be defined.
         */
        if (this.didLoad) {
            this.setupGesture();
            this.setupActivatedObserver();
        }
        const ionContent = findClosestIonContent(this.el);
        this.contentEl = ionContent?.querySelector('.ion-content-scroll-host') ?? ionContent;
    }
    disconnectedCallback() {
        if (this.gesture) {
            this.gesture.destroy();
            this.gesture = undefined;
        }
        if (this.activatedObserver) {
            this.activatedObserver.disconnect();
            this.activatedObserver = undefined;
        }
    }
    getValue() {
        const value = this.value ?? 0;
        if (this.dualKnobs) {
            if (typeof value === 'object') {
                return value;
            }
            return {
                lower: 0,
                upper: value,
            };
        }
        else {
            if (typeof value === 'object') {
                return value.upper;
            }
            return value;
        }
    }
    /**
     * Emits an `ionChange` event.
     *
     * This API should be called for user committed changes.
     * This API should not be used for external value changes.
     */
    emitValueChange() {
        this.value = this.ensureValueInBounds(this.value);
        this.ionChange.emit({ value: this.value });
    }
    /**
     * The value should be updated on touch end or
     * when the component is being dragged.
     * This follows the native behavior of mobile devices.
     *
     * For example: When the user lifts their finger from the
     * screen after tapping the bar or dragging the bar or knob.
     */
    onStart() {
        this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
    }
    /**
     * The value should be updated while dragging the
     * bar or knob.
     *
     * While the user is dragging, the view
     * should not scroll. This is to prevent the user from
     * feeling disoriented while dragging.
     *
     * The user can scroll on the view if the knob or
     * bar is not being dragged.
     *
     * @param detail The details of the gesture event.
     */
    onMove(detail) {
        const { contentEl, pressedKnob } = this;
        const currentX = detail.currentX;
        /**
         * Since the user is dragging on the bar or knob, the view should not scroll.
         *
         * This only needs to be done once.
         */
        if (contentEl && this.pressedKnob === undefined) {
            this.initialContentScrollY = disableContentScrollY(contentEl);
        }
        /**
         * The `pressedKnob` can be undefined if the user just
         * started dragging the knob.
         *
         * This is necessary to determine which knob the user is dragging,
         * especially when using dual knobs.
         * Plus, it determines when to apply certain styles.
         *
         * This only needs to be done once since the knob won't change
         * while the user is dragging.
         */
        if (pressedKnob === undefined) {
            this.setPressedKnob(currentX);
        }
        this.update(currentX);
    }
    /**
     * The value should be updated on touch end:
     * - When the user lifts their finger from the screen after
     * tapping the bar.
     *
     * @param detail The details of the gesture or mouse event.
     */
    onEnd(detail) {
        const { contentEl, initialContentScrollY } = this;
        const currentX = detail.currentX ?? detail.clientX;
        /**
         * The `pressedKnob` can be undefined if the user never
         * dragged the knob. They just tapped on the bar.
         *
         * This is necessary to determine which knob the user is changing,
         * especially when using dual knobs.
         * Plus, it determines when to apply certain styles.
         */
        if (this.pressedKnob === undefined) {
            this.setPressedKnob(currentX);
        }
        /**
         * The user is no longer dragging the bar or
         * knob (if they were dragging it).
         *
         * The user can now scroll on the view in the next gesture event.
         */
        if (contentEl && this.pressedKnob !== undefined) {
            resetContentScrollY(contentEl, initialContentScrollY);
        }
        // update the active knob's position
        this.update(currentX);
        /**
         * Reset the pressed knob to undefined since the user
         * may start dragging a different knob in the next gesture event.
         */
        this.pressedKnob = undefined;
        this.emitValueChange();
        this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
    }
    update(currentX) {
        // figure out where the pointer is currently at
        // update the knob being interacted with
        const rect = this.rect;
        let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
        if (isRTL(this.el)) {
            ratio = 1 - ratio;
        }
        if (this.snaps) {
            // snaps the ratio to the current value
            ratio = valueToRatio(ratioToValue(ratio, this.min, this.max, this.step), this.min, this.max);
        }
        // update which knob is pressed
        if (this.pressedKnob === 'A') {
            this.ratioA = ratio;
        }
        else {
            this.ratioB = ratio;
        }
        // Update input value
        this.updateValue();
    }
    setPressedKnob(currentX) {
        const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
        // figure out which knob they started closer to
        let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
        if (isRTL(this.el)) {
            ratio = 1 - ratio;
        }
        this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
    }
    get valA() {
        return ratioToValue(this.ratioA, this.min, this.max, this.step);
    }
    get valB() {
        return ratioToValue(this.ratioB, this.min, this.max, this.step);
    }
    get ratioLower() {
        if (this.dualKnobs) {
            return Math.min(this.ratioA, this.ratioB);
        }
        const { activeBarStart } = this;
        if (activeBarStart == null) {
            return 0;
        }
        return valueToRatio(activeBarStart, this.min, this.max);
    }
    get ratioUpper() {
        if (this.dualKnobs) {
            return Math.max(this.ratioA, this.ratioB);
        }
        return this.ratioA;
    }
    updateRatio() {
        const value = this.getValue();
        const { min, max } = this;
        /**
         * For dual knobs, value gives lower/upper but not which is A vs B.
         * Assign (lowerRatio, upperRatio) to (ratioA, ratioB) in the way that
         * minimizes change from the current ratios so the knobs don't swap.
         */
        if (this.dualKnobs) {
            const lowerRatio = valueToRatio(value.lower, min, max);
            const upperRatio = valueToRatio(value.upper, min, max);
            if (Math.abs(this.ratioA - lowerRatio) + Math.abs(this.ratioB - upperRatio) <=
                Math.abs(this.ratioA - upperRatio) + Math.abs(this.ratioB - lowerRatio)) {
                this.ratioA = lowerRatio;
                this.ratioB = upperRatio;
            }
            else {
                this.ratioA = upperRatio;
                this.ratioB = lowerRatio;
            }
        }
        else {
            this.ratioA = valueToRatio(value, min, max);
        }
    }
    updateValue() {
        this.noUpdate = true;
        const { valA, valB } = this;
        this.value = !this.dualKnobs
            ? valA
            : {
                lower: Math.min(valA, valB),
                upper: Math.max(valA, valB),
            };
        this.noUpdate = false;
    }
    /**
     * Returns true if content was passed to the "start" slot
     */
    get hasStartSlotContent() {
        return this.el.querySelector('[slot="start"]') !== null;
    }
    /**
     * Returns true if content was passed to the "end" slot
     */
    get hasEndSlotContent() {
        return this.el.querySelector('[slot="end"]') !== null;
    }
    get hasLabel() {
        return this.label !== undefined || this.el.querySelector('[slot="label"]') !== null;
    }
    renderRangeSlider() {
        const { min, max, step, handleKeyboard, activatedKnob, focusedKnob, hoveredKnob, pressedKnob, disabled, pin, ratioLower, ratioUpper, pinFormatter, inheritedAttributes, } = this;
        let barStart = `${ratioLower * 100}%`;
        let barEnd = `${100 - ratioUpper * 100}%`;
        const rtl = isRTL(this.el);
        const start = rtl ? 'right' : 'left';
        const end = rtl ? 'left' : 'right';
        const tickStyle = (tick) => {
            return {
                [start]: tick[start],
            };
        };
        if (this.dualKnobs === false) {
            /**
             * When the value is less than the activeBarStart or the min value,
             * the knob will display at the start of the active bar.
             */
            if (this.valA < (this.activeBarStart ?? this.min)) {
                /**
                 * Sets the bar positions relative to the upper and lower limits.
                 * Converts the ratio values into percentages, used as offsets for left/right styles.
                 *
                 * The ratioUpper refers to the knob position on the bar.
                 * The ratioLower refers to the end position of the active bar (the value).
                 */
                barStart = `${ratioUpper * 100}%`;
                barEnd = `${100 - ratioLower * 100}%`;
            }
            else {
                /**
                 * Otherwise, the knob will display at the end of the active bar.
                 *
                 * The ratioLower refers to the start position of the active bar (the value).
                 * The ratioUpper refers to the knob position on the bar.
                 */
                barStart = `${ratioLower * 100}%`;
                barEnd = `${100 - ratioUpper * 100}%`;
            }
        }
        const barStyle = {
            [start]: barStart,
            [end]: barEnd,
        };
        const ticks = [];
        if (this.snaps && this.ticks) {
            for (let value = min; value <= max; value += step) {
                const ratio = valueToRatio(value, min, max);
                const ratioMin = Math.min(ratioLower, ratioUpper);
                const ratioMax = Math.max(ratioLower, ratioUpper);
                const tick = {
                    ratio,
                    /**
                     * Sets the tick mark as active when the tick is between the min bounds and the knob.
                     * When using activeBarStart, the tick mark will be active between the knob and activeBarStart.
                     */
                    active: ratio >= ratioMin && ratio <= ratioMax,
                };
                tick[start] = `${ratio * 100}%`;
                ticks.push(tick);
            }
        }
        return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl), onPointerDown: () => {
                this.focusFromPointer = true;
            },
            /**
             * Since the gesture has a threshold, the value
             * won't change until the user has dragged past
             * the threshold. This is to prevent the range
             * from moving when the user is scrolling.
             *
             * This results in the value not being updated
             * and the event emitters not being triggered
             * if the user taps on the range. This is why
             * we need to listen for the "pointerUp" event.
             */
            onPointerUp: (ev) => {
                this.focusFromPointer = false;
                /**
                 * If the user drags the knob on the web
                 * version (does not occur on mobile),
                 * the "pointerUp" event will be triggered
                 * along with the gesture's events.
                 * This leads to duplicate events.
                 *
                 * By checking if the pressedKnob is undefined,
                 * we can determine if the "pointerUp" event was
                 * triggered by a tap or a drag. If it was
                 * dragged, the pressedKnob will be defined.
                 */
                if (this.pressedKnob === undefined) {
                    this.onStart();
                    this.onEnd(ev);
                }
            } }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
                'range-tick': true,
                'range-tick-active': tick.active,
            }, part: tick.active ? 'tick-active' : 'tick' }))), h("div", { class: "range-bar-container" }, h("div", { class: "range-bar", role: "presentation", part: "bar" }), h("div", { class: {
                'range-bar': true,
                'range-bar-active': true,
                'has-ticks': ticks.length > 0,
            }, role: "presentation", style: barStyle, part: "bar-active" })), renderKnob(rtl, {
            knob: 'A',
            position: getKnobPosition('A', this.ratioA, this.ratioB, this.dualKnobs),
            dualKnobs: this.dualKnobs,
            activated: activatedKnob === 'A',
            focused: focusedKnob === 'A',
            hovered: hoveredKnob === 'A',
            pressed: pressedKnob === 'A',
            value: this.valA,
            ratio: this.ratioA,
            pin,
            pinFormatter,
            disabled,
            handleKeyboard,
            min,
            max,
            inheritedAttributes,
            onKnobFocus: this.onKnobFocus,
            onKnobBlur: this.onKnobBlur,
            onKnobMouseEnter: this.onKnobMouseEnter,
            onKnobMouseLeave: this.onKnobMouseLeave,
        }), this.dualKnobs &&
            renderKnob(rtl, {
                knob: 'B',
                position: getKnobPosition('B', this.ratioA, this.ratioB, this.dualKnobs),
                dualKnobs: this.dualKnobs,
                activated: activatedKnob === 'B',
                focused: focusedKnob === 'B',
                hovered: hoveredKnob === 'B',
                pressed: pressedKnob === 'B',
                value: this.valB,
                ratio: this.ratioB,
                pin,
                pinFormatter,
                disabled,
                handleKeyboard,
                min,
                max,
                inheritedAttributes,
                onKnobFocus: this.onKnobFocus,
                onKnobBlur: this.onKnobBlur,
                onKnobMouseEnter: this.onKnobMouseEnter,
                onKnobMouseLeave: this.onKnobMouseLeave,
            })));
    }
    render() {
        const { disabled, el, hasLabel, rangeId, pin, pressedKnob, labelPlacement, label, dualKnobs, min, max } = this;
        const inItem = hostContext('ion-item', el);
        /**
         * If there is no start content then the knob at
         * the min value will be cut off by the item margin.
         */
        const hasStartContent = (hasLabel && (labelPlacement === 'start' || labelPlacement === 'fixed')) || this.hasStartSlotContent;
        const needsStartAdjustment = inItem && !hasStartContent;
        /**
         * If there is no end content then the knob at
         * the max value will be cut off by the item margin.
         */
        const hasEndContent = (hasLabel && labelPlacement === 'end') || this.hasEndSlotContent;
        const needsEndAdjustment = inItem && !hasEndContent;
        const mode = getIonMode(this);
        /**
         * Determine the name and position of the pressed knob to apply
         * Host classes for styling.
         */
        const pressedKnobName = dualKnobs ? pressedKnob?.toLowerCase() : undefined;
        const pressedKnobPosition = dualKnobs && pressedKnob ? getKnobPosition(pressedKnob, this.ratioA, this.ratioB, dualKnobs) : undefined;
        /**
         * Determine if any knob is at the min or max value to
         * apply Host classes for styling.
         */
        const valueAtMin = dualKnobs ? this.valA === min || this.valB === min : this.valA === min;
        const valueAtMax = dualKnobs ? this.valA === max || this.valB === max : this.valA === max;
        renderHiddenInput(true, el, this.name, JSON.stringify(this.getValue()), disabled);
        return (h(Host, { key: '72f253ee1514b4c5874a6720c2f954fae7a7c4d4', onFocusin: this.onFocus, onFocusout: this.onBlur, id: rangeId, class: createColorClasses(this.color, {
                [mode]: true,
                'in-item': inItem,
                'range-disabled': disabled,
                'range-dual-knobs': dualKnobs,
                'range-pressed': pressedKnob !== undefined,
                [`range-pressed-${pressedKnobName}`]: pressedKnob !== undefined && pressedKnobName !== undefined,
                [`range-pressed-${pressedKnobPosition}`]: pressedKnob !== undefined && pressedKnobPosition !== undefined,
                'range-has-pin': pin,
                [`range-label-placement-${labelPlacement}`]: true,
                'range-item-start-adjustment': needsStartAdjustment,
                'range-item-end-adjustment': needsEndAdjustment,
                'range-value-min': valueAtMin,
                'range-value-max': valueAtMax,
            }) }, h("label", { key: '9b96b83a6118ee2a79d8adbb0a58197470308179', class: "range-wrapper", id: "range-label" }, h("div", { key: 'c7b31a7c87c20857a26624bd94501df6420fceec', class: {
                'label-text-wrapper': true,
                'label-text-wrapper-hidden': !hasLabel,
            }, part: "label" }, label !== undefined ? h("div", { class: "label-text" }, label) : h("slot", { name: "label" })), h("div", { key: '4ca15b9370097e140550489ff24979f3a959e65b', class: "native-wrapper" }, h("slot", { key: 'f110896c68741bad92239f98b6028cbe0ad5122f', name: "start" }), this.renderRangeSlider(), h("slot", { key: '5c4031c4403fa643ee2e11637b2420ddd8cbc4ce', name: "end" })))));
    }
    static get is() { return "ion-range"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "ios": ["range.ios.scss"],
            "md": ["range.md.scss"]
        };
    }
    static get styleUrls() {
        return {
            "ios": ["range.ios.css"],
            "md": ["range.md.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",
                            "referenceLocation": "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"
            },
            "debounce": {
                "type": "number",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "How long, in milliseconds, to wait to trigger the\n`ionInput` event after each change in the range value."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "debounce"
            },
            "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.rangeId"
            },
            "label": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The text to display as the control's label. Use this over the `label` slot if\nyou only need plain text. The `label` property will take priority over the\n`label` slot if both are used."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "label"
            },
            "dualKnobs": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the range shows two knobs and `value` is an object with `lower`\nand `upper` properties. If `false`, the range shows one knob and `value` is\na number."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "dual-knobs",
                "defaultValue": "false"
            },
            "min": {
                "type": "number",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Minimum integer value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "min",
                "defaultValue": "0"
            },
            "max": {
                "type": "number",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Maximum integer value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "max",
                "defaultValue": "100"
            },
            "pin": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, a pin with integer value is shown when the knob\nis pressed."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "pin",
                "defaultValue": "false"
            },
            "pinFormatter": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "PinFormatter",
                    "resolved": "(value: number) => string | number",
                    "references": {
                        "PinFormatter": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::PinFormatter",
                            "referenceLocation": "PinFormatter"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "A callback used to format the pin text.\nBy default the pin text is set to `Math.round(value)`.\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback."
                },
                "getter": false,
                "setter": false,
                "defaultValue": "(value: number): number => Math.round(value)"
            },
            "snaps": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, the knob snaps to tick marks evenly spaced based\non the step property value."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "snaps",
                "defaultValue": "false"
            },
            "step": {
                "type": "number",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Specifies the value granularity."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "step",
                "defaultValue": "1"
            },
            "ticks": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "If `true`, tick marks are displayed based on the step value.\nOnly applies when `snaps` is `true`."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "ticks",
                "defaultValue": "true"
            },
            "activeBarStart": {
                "type": "number",
                "mutable": true,
                "complexType": {
                    "original": "number",
                    "resolved": "number | undefined",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The start position of the range active bar. This feature is only available with a single knob (dualKnobs=\"false\").\nValid values are greater than or equal to the min value and less than or equal to the max value."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "active-bar-start"
            },
            "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 range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "disabled",
                "defaultValue": "false"
            },
            "value": {
                "type": "number",
                "mutable": true,
                "complexType": {
                    "original": "RangeValue",
                    "resolved": "number | { lower: number; upper: number; }",
                    "references": {
                        "RangeValue": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::RangeValue",
                            "referenceLocation": "RangeValue"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "the value of the range."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "value",
                "defaultValue": "0"
            },
            "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 range.\n`\"start\"`: The label will appear to the left of the range in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the range 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 range regardless of the direction."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "label-placement",
                "defaultValue": "'start'"
            }
        };
    }
    static get states() {
        return {
            "ratioA": {},
            "ratioB": {},
            "activatedKnob": {},
            "focusedKnob": {},
            "hoveredKnob": {},
            "pressedKnob": {}
        };
    }
    static get events() {
        return [{
                "method": "ionChange",
                "name": "ionChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "The `ionChange` event is fired for `<ion-range>` elements when the user\nmodifies the element's value:\n- When the user releases the knob after dragging;\n- When the user moves the knob with keyboard arrows\n\nThis event will not emit when programmatically setting the `value` property."
                },
                "complexType": {
                    "original": "RangeChangeEventDetail",
                    "resolved": "RangeChangeEventDetail",
                    "references": {
                        "RangeChangeEventDetail": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::RangeChangeEventDetail",
                            "referenceLocation": "RangeChangeEventDetail"
                        }
                    }
                }
            }, {
                "method": "ionInput",
                "name": "ionInput",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "The `ionInput` event is fired for `<ion-range>` elements when the value\nis modified. Unlike `ionChange`, `ionInput` is fired continuously\nwhile the user is dragging the knob."
                },
                "complexType": {
                    "original": "RangeChangeEventDetail",
                    "resolved": "RangeChangeEventDetail",
                    "references": {
                        "RangeChangeEventDetail": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::RangeChangeEventDetail",
                            "referenceLocation": "RangeChangeEventDetail"
                        }
                    }
                }
            }, {
                "method": "ionFocus",
                "name": "ionFocus",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the range has focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }, {
                "method": "ionBlur",
                "name": "ionBlur",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the range loses focus."
                },
                "complexType": {
                    "original": "void",
                    "resolved": "void",
                    "references": {}
                }
            }, {
                "method": "ionKnobMoveStart",
                "name": "ionKnobMoveStart",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the user starts moving the range knob, whether through\nmouse drag, touch gesture, or keyboard interaction."
                },
                "complexType": {
                    "original": "RangeKnobMoveStartEventDetail",
                    "resolved": "RangeKnobMoveStartEventDetail",
                    "references": {
                        "RangeKnobMoveStartEventDetail": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::RangeKnobMoveStartEventDetail",
                            "referenceLocation": "RangeKnobMoveStartEventDetail"
                        }
                    }
                }
            }, {
                "method": "ionKnobMoveEnd",
                "name": "ionKnobMoveEnd",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the user finishes moving the range knob, whether through\nmouse drag, touch gesture, or keyboard interaction."
                },
                "complexType": {
                    "original": "RangeKnobMoveEndEventDetail",
                    "resolved": "RangeKnobMoveEndEventDetail",
                    "references": {
                        "RangeKnobMoveEndEventDetail": {
                            "location": "import",
                            "path": "./range-interface",
                            "id": "src/components/range/range-interface.ts::RangeKnobMoveEndEventDetail",
                            "referenceLocation": "RangeKnobMoveEndEventDetail"
                        }
                    }
                }
            }];
    }
    static get elementRef() { return "el"; }
    static get watchers() {
        return [{
                "propName": "debounce",
                "methodName": "debounceChanged"
            }, {
                "propName": "dualKnobs",
                "methodName": "dualKnobsChanged"
            }, {
                "propName": "min",
                "methodName": "minChanged"
            }, {
                "propName": "max",
                "methodName": "maxChanged"
            }, {
                "propName": "step",
                "methodName": "stepChanged"
            }, {
                "propName": "activeBarStart",
                "methodName": "activeBarStartChanged"
            }, {
                "propName": "disabled",
                "methodName": "disabledChanged"
            }, {
                "propName": "value",
                "methodName": "valueChanged"
            }];
    }
}
const renderKnob = (rtl, { knob, position, dualKnobs, value, ratio, min, max, disabled, activated, focused, hovered, pressed, pin, handleKeyboard, pinFormatter, inheritedAttributes, onKnobFocus, onKnobBlur, onKnobMouseEnter, onKnobMouseLeave, }) => {
    const start = rtl ? 'right' : 'left';
    const knobStyle = () => {
        const style = {};
        style[start] = `${ratio * 100}%`;
        return style;
    };
    // The aria label should be preferred over visible text if both are specified
    const ariaLabel = inheritedAttributes['aria-label'];
    return (h("div", { onKeyDown: (ev) => {
            const key = ev.key;
            if (key === 'ArrowLeft' || key === 'ArrowDown') {
                handleKeyboard(knob, false);
                ev.preventDefault();
                ev.stopPropagation();
            }
            else if (key === 'ArrowRight' || key === 'ArrowUp') {
                handleKeyboard(knob, true);
                ev.preventDefault();
                ev.stopPropagation();
            }
        }, onFocus: () => onKnobFocus(knob), onBlur: onKnobBlur, onMouseEnter: () => onKnobMouseEnter(knob), onMouseLeave: onKnobMouseLeave, class: {
            'range-knob-handle': true,
            'range-knob-handle-a': knob === 'A',
            'range-knob-handle-b': knob === 'B',
            'range-knob-pressed': pressed,
            'range-knob-min': value === min,
            'range-knob-max': value === max,
            'ion-activatable': true,
            'ion-focusable': true,
            'ion-focused': focused,
        }, part: [
            'knob-handle',
            dualKnobs && knob === 'A' && 'knob-handle-a',
            dualKnobs && knob === 'B' && 'knob-handle-b',
            dualKnobs && position === 'lower' && 'knob-handle-lower',
            dualKnobs && position === 'upper' && 'knob-handle-upper',
            pressed && 'pressed',
            focused && 'focused',
            hovered && 'hover',
            activated && 'activated',
        ]
            .filter(Boolean)
            .join(' '), style: knobStyle(), role: "slider", tabindex: disabled ? -1 : 0, "aria-label": ariaLabel !== undefined ? ariaLabel : null, "aria-labelledby": ariaLabel === undefined ? 'range-label' : null, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled ? 'true' : null, "aria-valuenow": value }, pin && (h("div", { class: "range-pin", role: "presentation", part: [
            'pin',
            dualKnobs && knob === 'A' && 'pin-a',
            dualKnobs && knob === 'B' && 'pin-b',
            dualKnobs && position === 'lower' && 'pin-lower',
            dualKnobs && position === 'upper' && 'pin-upper',
            pressed && 'pressed',
            focused && 'focused',
            hovered && 'hover',
            activated && 'activated',
        ]
            .filter(Boolean)
            .join(' ') }, pinFormatter(value))), h("div", { class: "range-knob", role: "presentation", part: [
            'knob',
            dualKnobs && knob === 'A' && 'knob-a',
            dualKnobs && knob === 'B' && 'knob-b',
            dualKnobs && position === 'lower' && 'knob-lower',
            dualKnobs && position === 'upper' && 'knob-upper',
            pressed && 'pressed',
            focused && 'focused',
            hovered && 'hover',
            activated && 'activated',
        ]
            .filter(Boolean)
            .join(' ') })));
};
/**
 * Returns whether the given knob is at the lower or upper position based
 * on current ratios for the given knob.
 *
 * When both knobs have the same ratio, we only want one "lower" and one
 * "upper" position so that the `lower` and `upper` parts are not applied to
 * the same knob. In that case, we treat knob "A" as the lower position and
 * knob "B" as the upper position.
 */
const getKnobPosition = (knob, ratioA, ratioB, dualKnobs) => {
    if (!dualKnobs) {
        return 'lower';
    }
    if (ratioA === ratioB) {
        return knob === 'A' ? 'lower' : 'upper';
    }
    if (knob === 'A') {
        return ratioA < ratioB ? 'lower' : 'upper';
    }
    return ratioB < ratioA ? 'lower' : 'upper';
};
const ratioToValue = (ratio, min, max, step) => {
    let value = (max - min) * ratio;
    if (step > 0) {
        // round to nearest multiple of step, then add min
        value = Math.round(value / step) * step + min;
    }
    const clampedValue = clamp(min, value, max);
    return roundToMaxDecimalPlaces(clampedValue, min, max, step);
};
const valueToRatio = (value, min, max) => {
    return clamp(0, (value - min) / (max - min), 1);
};
let rangeIds = 0;