UNPKG

@alyle/ui

Version:

Minimal Design, a set of components for Angular

1,143 lines 53.8 kB
import { __decorate } from 'tslib';
import { trigger, transition, animate, keyframes, style } from '@angular/animations';
import * as i0 from '@angular/core';
import { Directive, isDevMode, EventEmitter, Input, Output, ContentChild, forwardRef, ContentChildren, ViewChild, Optional, Self, ChangeDetectionStrategy, Component, HostListener, NgModule } from '@angular/core';
import * as i2 from '@alyle/ui/field';
import { STYLES as STYLES$1, LyFieldControlBase } from '@alyle/ui/field';
import * as i1 from '@alyle/ui';
import { shadowBuilder, st2c, StyleCollection, mixinTabIndex, Dir, LyOverlayPosition, YPosition, XPosition, StyleRenderer, mixinDisableRipple, Style, LyCommonModule, LyOverlayModule } from '@alyle/ui';
import { Subject, defer, merge } from 'rxjs';
import { startWith, switchMap, take, filter, mapTo, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import { ESCAPE, DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE, hasModifierKey, A } from '@angular/cdk/keycodes';
import { coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
import { SelectionModel } from '@angular/cdk/collections';
import * as i3 from '@angular/cdk/scrolling';
import * as i4 from '@angular/forms';
import * as i5 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i6 from '@angular/cdk/platform';
import * as i7 from '@alyle/ui/checkbox';
import { LyCheckboxModule } from '@alyle/ui/checkbox';

/**
 * Returns an exception to be thrown when attempting to assign a non-array value to a select
 * in `multiple` mode. Note that `undefined` and `null` are still valid values to allow for
 * resetting the value.
 * @docs-private
 */
function getLySelectNonArrayValueError() {
    return Error('Value must be an array in multiple-selection mode.');
}
/**
 * Returns an exception to be thrown when assigning a non-function value to the comparator
 * used to determine if a value corresponds to an option. Note that whether the function
 * actually takes two values and returns a boolean is not checked.
 */
function getLySelectNonFunctionValueError() {
    return Error('`compareWith` must be a function.');
}

const DEFAULT_DISABLE_RIPPLE = false;
const STYLE_PRIORITY = -2;
const STYLES = (theme, ref) => {
    ref.renderStyleSheet(STYLES$1);
    const field = ref.selectorsOf(STYLES$1);
    const select = ref.selectorsOf(STYLES);
    const { after } = theme;
    return {
        $priority: STYLE_PRIORITY,
        $name: LySelect.и,
        root: () => (_className) => `${_className}{display:block;position:relative;padding-${after}:1em;min-height:1em;-webkit-tap-highlight-color:transparent;}${st2c(((theme.select
            && theme.select.root
            && (theme.select.root instanceof StyleCollection
                ? theme.select.root.setTransformer(fn => fn(select))
                : theme.select.root(select)))), `${_className}`)}`,
        container: (_className) => `${_className}{background:${theme.background.primary.default};border-radius:2px;box-shadow:${shadowBuilder(4)};display:block;transform-origin:inherit;pointer-events:all;overflow:auto;max-height:256px;}`,
        valueText: (_className) => `${_className}{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}`,
        option: (_className) => `${_className}{display:flex;font-family:${theme.typography.fontFamily};color:${theme.text.default};-webkit-tap-highlight-color:transparent;background-color:rgba(0,0,0,0);border:0;padding:0 1em;margin:0;outline:none;box-sizing:border-box;position:relative;justify-content:flex-start;align-items:center;align-content:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:1.125;height:3em;cursor:pointer;}`,
        optionActive: (_className) => `${_className}{background:${theme.hover};}`,
        optionText: (_className) => `ly-checkbox ~ ${_className}{display:flex;align-items:inherit;align-content:inherit;}`,
        content: (_className) => `${_className}{padding:0;display:flex;justify-content:inherit;align-items:inherit;align-content:inherit;width:100%;height:100%;box-sizing:border-box;}`,
        /** Style for field */
        _cursorPointer: () => (_className) => `${_className}:not(${field.disabled}) ${field.container}{cursor:pointer;}`,
    };
};
/** Change event object that is emitted when the select value has changed. */
class LySelectChange {
    constructor(
    /** Reference to the select that emitted the change event. */
    source, 
    /** Current value of the select that emitted the event. */
    value) {
        this.source = source;
        this.value = value;
    }
}
/** @docs-private */
const ANIMATIONS = [
    trigger('selectEnter', [
        transition('void => in', [
            animate('125ms cubic-bezier(0, 0, 0.2, 1)', keyframes([
                style({
                    opacity: 0,
                    transform: 'scaleY(0.9)'
                }),
                style({
                    opacity: 1,
                    transform: 'scaleY(1)'
                })
            ]))
        ]),
        transition('* => void', animate('100ms 25ms linear', style({ opacity: 0 })))
    ]),
];
/** @docs-private */
class LySelectBase {
}
/** @docs-private */
const LySelectMixinBase = mixinTabIndex(LySelectBase);
/**
 * Allows the user to customize the trigger that is displayed when the select has a value.
 */
class LySelectTrigger {
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.10", type: LySelectTrigger, isStandalone: false, selector: "ly-select-trigger", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectTrigger, decorators: [{
            type: Directive,
            args: [{
                    selector: 'ly-select-trigger',
                    standalone: false
                }]
        }] });
class LySelect extends LySelectMixinBase {
    /** @docs-private */
    static { this.и = 'LySelect'; }
    _onFocus() {
        if (!this.disabled) {
            this._focused = true;
            this.stateChanges.next();
        }
    }
    _onBlur() {
        this._focused = false;
        if (!this.disabled && !this._opened) {
            this.onTouched();
            this._cd.markForCheck();
            this.stateChanges.next();
        }
    }
    /** Time to wait in milliseconds after the last keystroke before moving focus to an item. */
    get typeaheadDebounceInterval() { return this._typeaheadDebounceInterval; }
    set typeaheadDebounceInterval(value) {
        const newVal = coerceNumberProperty(value);
        if (this._typeaheadDebounceInterval !== newVal && this._keyManager) {
            this._typeaheadDebounceInterval = newVal;
            this._keyManager.withTypeAhead(newVal);
        }
    }
    /** Value of the select control. */
    get value() { return this._value; }
    set value(newValue) {
        if (newValue !== this._value) {
            if (this.options) {
                this._setSelectionByValue(newValue);
            }
            this._value = newValue;
        }
    }
    /** Whether the input is disabled. */
    set disabled(val) {
        if (val !== this._disabled) {
            this._disabled = coerceBooleanProperty(val);
            if (this._field) {
                if (!val && this._hasDisabledClass) {
                    this._renderer.removeClass(this._field._getHostElement(), this._field.classes.disabled);
                    this._field.sRenderer.addClass(this.classes._cursorPointer);
                    this._hasDisabledClass = undefined;
                }
                else if (val) {
                    this._renderer.addClass(this._field._getHostElement(), this._field.classes.disabled);
                    this._field.sRenderer.removeClass(this.classes._cursorPointer);
                    this._hasDisabledClass = true;
                }
            }
            this.stateChanges.next();
        }
    }
    get disabled() {
        if (this.ngControl && this.ngControl.disabled !== null) {
            return this.ngControl.disabled;
        }
        return this._disabled;
    }
    set required(value) {
        this._required = coerceBooleanProperty(value);
    }
    get required() { return this._required; }
    set multiple(value) {
        this._multiple = coerceBooleanProperty(value);
    }
    get multiple() { return this._multiple; }
    /**
     * @deprecated has been deprecated in favor of `compareWith`
     */
    set valueKey(fn) {
        this._valueKey = fn;
        console.warn('LySelect: `[valueKey]` has been deprecated in favor of `[compareWith]`');
    }
    get valueKey() { return this._valueKey; }
    set placeholder(val) {
        this._placeholder = val;
    }
    get placeholder() { return this._placeholder; }
    /**
     * Function to compare the option values with the selected values. The first argument
     * is a value from an option. The second is a value from the selection. A boolean
     * should be returned.
     */
    get compareWith() { return this._compareWith; }
    set compareWith(fn) {
        if (typeof fn !== 'function' && isDevMode()) {
            throw getLySelectNonFunctionValueError();
        }
        this._compareWith = fn;
        if (this._selectionModel) {
            // A different comparator means the selection could change.
            this._initializeSelection();
        }
    }
    get focused() {
        return this._focused;
    }
    get empty() {
        return !this._selectionModel || this._selectionModel.isEmpty();
    }
    get floatingLabel() {
        return this._opened ? true : !this.empty;
    }
    /** The value displayed in the trigger. */
    get triggerValue() {
        if (this._multiple) {
            const selectedOptions = this._selectionModel.selected.map(option => option.viewValue);
            if (this._theme.variables.direction === Dir.rtl) {
                selectedOptions.reverse();
            }
            return selectedOptions.join(', ');
        }
        return this._selectionModel.selected[0].viewValue;
    }
    /** Current selecteds */
    get selected() {
        const selected = this._selectionModel?.selected;
        return this.multiple ? (selected || []) : selected[0];
    }
    constructor(_theme, sRenderer, _renderer, _el, _overlay, 
    /** @internal */
    _field, 
    /** @internal */
    _cd, _ngZone, _viewportRuler, 
    /** @docs-private */
    ngControl, _parentForm, _parentFormGroup) {
        super();
        this._theme = _theme;
        this.sRenderer = sRenderer;
        this._renderer = _renderer;
        this._el = _el;
        this._overlay = _overlay;
        this._field = _field;
        this._cd = _cd;
        this._ngZone = _ngZone;
        this._viewportRuler = _viewportRuler;
        this.ngControl = ngControl;
        this._parentForm = _parentForm;
        this._parentFormGroup = _parentFormGroup;
        /** @docs-private */
        this.classes = this.sRenderer.renderSheet(STYLES);
        /** The cached font-size of the trigger element. */
        this._triggerFontSize = 0;
        this._disabled = false;
        this._required = false;
        this.stateChanges = new Subject();
        this._form = this._parentForm || this._parentFormGroup;
        this._valueKey = same;
        this._previousOffsetY = 0;
        this._focused = false;
        this.errorState = false;
        /** Emits when the panel element is finished transforming in. */
        this._panelDoneAnimatingStream = new Subject();
        /** Comparison function to specify which option is displayed. Defaults to object equality. */
        this._compareWith = (o1, o2) => o1 === o2;
        /** Emits whenever the component is destroyed. */
        this._destroy = new Subject();
        /** Combined stream of all of the child options' change events. */
        this.optionSelectionChanges = defer(() => {
            const options = this.options;
            if (options) {
                return options.changes.pipe(startWith(options), switchMap(() => merge(...options.map(option => option.onSelectionChange))));
            }
            return this._ngZone.onStable
                .pipe(take(1), switchMap(() => this.optionSelectionChanges));
        });
        /** Event emitted when the select panel has been toggled. */
        this.openedChange = new EventEmitter();
        /** Event emitted when the select has been opened. */
        this._openedStream = this.openedChange.pipe(filter(o => o), mapTo(null));
        /** Event emitted when the select has been closed. */
        this._closedStream = this.openedChange.pipe(filter(o => !o), mapTo(null));
        /** Event emitted when the selected value has been changed by the user. */
        this.selectionChange = new EventEmitter();
        /**
         * Event that emits whenever the raw value of the select changes. This is here primarily
         * to facilitate the two-way binding for the `value` input.
         * @docs-private
         */
        this.valueChange = new EventEmitter();
        /**
         * The registered callback function called when a change event occurs on the input element.
         */
        this.onChange = (_) => { };
        /**
         * The registered callback function called when a blur event occurs on the input element.
         */
        this.onTouched = () => { };
        if (this.ngControl) {
            // Note: we provide the value accessor through here, instead of
            // the `providers` to avoid running into a circular import.
            this.ngControl.valueAccessor = this;
        }
        this._field?.sRenderer.addClass(this.classes._cursorPointer);
    }
    ngOnInit() {
        this._selectionModel = new SelectionModel(this.multiple);
        this.stateChanges.next();
        // We need `distinctUntilChanged` here, because some browsers will
        // fire the animation end event twice for the same animation. See:
        // https://github.com/angular/angular/issues/24084
        this._panelDoneAnimatingStream
            .pipe(distinctUntilChanged((prev, curr) => prev.state === curr.state), takeUntil(this._destroy))
            .subscribe((re) => {
            if (this._opened) {
                this.openedChange.emit(true);
            }
            if (re.state === 'void') {
                if (!re.overlay.isDestroyed) {
                    re.overlay.destroy();
                    this.openedChange.emit(false);
                    this.stateChanges.next();
                    this._cd.markForCheck();
                }
            }
        });
        const ngControl = this.ngControl;
        // update styles on disabled
        if (ngControl && ngControl.statusChanges) {
            ngControl.statusChanges.pipe(takeUntil(this._destroy)).subscribe(() => {
                this.disabled = !!ngControl.disabled;
            });
        }
        // apply class {selectArrow} to `<select>`
        this._renderer.addClass(this._field._getHostElement(), this._field.classes.selectArrow);
        // apply default styles
        this._renderer.addClass(this._el.nativeElement, this._field.classes.inputNative);
        this._renderer.addClass(this._el.nativeElement, this.classes.root);
    }
    ngDoCheck() {
        const ngControl = this.ngControl;
        if (ngControl) {
            // The disabled state might go out of sync if the form group is swapped out. See #17860.
            if (this._previousControl !== ngControl.control) {
                if (this._previousControl !== undefined &&
                    ngControl.disabled !== null &&
                    ngControl.disabled !== this.disabled) {
                    this.disabled = ngControl.disabled;
                }
                this._previousControl = ngControl.control;
            }
            const oldVal = this.errorState;
            const newVal = !!(this.ngControl && this.ngControl.invalid && (this.ngControl.touched || (this._form && this._form.submitted)));
            if (newVal !== oldVal) {
                this.errorState = newVal;
                if (this._field) {
                    const errorClass = this._field.classes.errorState;
                    if (newVal) {
                        this._renderer.addClass(this._field._getHostElement(), errorClass);
                        this._errorClass = errorClass;
                    }
                    else if (this._errorClass) {
                        this._renderer.removeClass(this._field._getHostElement(), errorClass);
                        this._errorClass = undefined;
                    }
                    this.stateChanges.next();
                }
            }
        }
    }
    ngAfterContentInit() {
        this._initKeyManager();
        this.options.changes.pipe(startWith(null), takeUntil(this._destroy)).subscribe(() => {
            this._resetOptions();
            this._initializeSelection();
        });
        this._selectionModel.changed.pipe(takeUntil(this._destroy)).subscribe(event => {
            event.added.forEach(option => option.select());
            event.removed.forEach(option => option.deselect());
        });
        Promise.resolve().then(() => {
            this.value = this.ngControl ? this.ngControl.value : this._value;
            this.stateChanges.next();
            this._cd.markForCheck();
        });
        this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
            if (!this._opened && !this.multiple && this._keyManager.activeItem) {
                this._keyManager.activeItem._selectViaInteraction();
            }
        });
    }
    ngOnDestroy() {
        this._destroy.next();
        this._destroy.complete();
        this.stateChanges.complete();
        this._panelDoneAnimatingStream.complete();
        if (this._overlayRef) {
            this._overlayRef.destroy();
            this._overlayRef = null;
        }
    }
    open() {
        if (this.disabled || !this.options || !this.options.length || this._opened) {
            return;
        }
        if (this._overlayRef) {
            this._overlayRef.destroy();
        }
        this._opened = true;
        const overlayRef = this._overlay.create(this.templateRef, {
            $implicit: () => overlayRef
        }, {
            styles: {
                top: 0,
                left: 0,
                pointerEvents: null
            },
            fnDestroy: () => {
                this._closeOverlay(overlayRef);
                keydownEventsSuscription.unsubscribe();
                this.focus();
            },
            onResizeScroll: this._updatePlacement.bind(this)
        });
        this._overlayRef = overlayRef;
        const keydownEvents = overlayRef.keydownEvents();
        const keydownEventsSuscription = keydownEvents.subscribe((event) => {
            if (event.keyCode === ESCAPE) {
                this._closeOverlay(overlayRef);
                keydownEventsSuscription.unsubscribe();
            }
        });
        this._keyManager.withHorizontalOrientation(null);
        this._triggerFontSize = parseInt(getComputedStyle(this._getHostElement()).fontSize || '0');
        this._highlightCorrectOption();
        this._cd.markForCheck();
        this.stateChanges.next();
        this._ngZone.onStable.pipe(take(1)).subscribe(() => this._updatePlacement(true));
    }
    close() {
        const overlayRef = this._overlayRef;
        if (overlayRef) {
            this._overlayRef = null;
            this._closeOverlay(overlayRef);
        }
    }
    _closeOverlay(overlay) {
        const overlayRef = overlay;
        if (overlayRef && !overlay.isDestroyed) {
            this._opened = false;
            this._keyManager.withHorizontalOrientation(this._theme.variables.direction);
            overlayRef.detach();
            this._cd.markForCheck();
            this.onTouched();
            // Make sure to destroy the overlayRef
            this._ngZone.runOutsideAngular(() => {
                setTimeout(() => {
                    overlayRef.destroy();
                }, 250);
            });
        }
    }
    /** @docs-private */
    onContainerClick() {
        this.focus();
        this.open();
    }
    /** Focuses the select element. */
    focus(options) {
        this._getHostElement().focus(options);
    }
    _getHostElement() {
        return this._el.nativeElement;
    }
    /**
     * Sets the "value" property on the input element.
     *
     * @param value The checked value
     */
    writeValue(value) {
        this.value = value;
    }
    /**
     * Registers a function called when the control value changes.
     *
     * @param fn The callback function
     */
    registerOnChange(fn) {
        this.onChange = (_valueString) => {
            fn(this.value);
        };
    }
    /**
     * Registers a function called when the control is touched.
     *
     * @param fn The callback function
     */
    registerOnTouched(fn) { this.onTouched = fn; }
    /**
     * Disables the select. Part of the ControlValueAccessor interface required
     * to integrate with Angular's core forms API.
     *
     * @param isDisabled Sets whether the component is disabled.
     */
    setDisabledState(isDisabled) {
        this.disabled = isDisabled;
        this._cd.markForCheck();
        this.stateChanges.next();
    }
    /** Handles all keydown events on the select. */
    _handleKeydown(event) {
        if (!this.disabled) {
            this._opened ? this._handleOpenKeydown(event) : this._handleClosedKeydown(event);
        }
    }
    /** Handles keyboard events while the select is closed. */
    _handleClosedKeydown(event) {
        const keyCode = event.keyCode;
        const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ||
            keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW;
        const isOpenKey = keyCode === ENTER || keyCode === SPACE;
        const manager = this._keyManager;
        // Open the select on ALT + arrow key to match the native <select>
        if (!manager.isTyping() && (isOpenKey && !hasModifierKey(event)) ||
            ((this.multiple || event.altKey) && isArrowKey)) {
            event.preventDefault(); // prevents the page from scrolling down when pressing space
            this.open();
        }
        else if (!this.multiple) {
            manager.onKeydown(event);
        }
    }
    /** Handles keyboard events when the selected is open. */
    _handleOpenKeydown(event) {
        const manager = this._keyManager;
        const keyCode = event.keyCode;
        const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
        const isTyping = manager.isTyping();
        if (isArrowKey && event.altKey) {
            // Close the select on ALT + arrow key to match the native <select>
            event.preventDefault();
            this.close();
            // Don't do anything in this case if the user is typing,
            // because the typing sequence can include the space key.
        }
        else if (!isTyping && (keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
            !hasModifierKey(event)) {
            event.preventDefault();
            manager.activeItem._selectViaInteraction();
        }
        else if (!isTyping && this._multiple && keyCode === A && event.ctrlKey) {
            event.preventDefault();
            const hasDeselectedOptions = this.options.some(opt => !opt.disabled && !opt.selected);
            this.options.forEach(option => {
                if (!option.disabled) {
                    hasDeselectedOptions ? option.select() : option.deselect();
                }
            });
        }
        else {
            const previouslyFocusedIndex = manager.activeItemIndex;
            manager.onKeydown(event);
            if (this._multiple && isArrowKey && event.shiftKey && manager.activeItem &&
                manager.activeItemIndex !== previouslyFocusedIndex) {
                manager.activeItem._selectViaInteraction();
            }
        }
    }
    _initializeSelection() {
        // Defer setting the value in order to avoid the "Expression
        // has changed after it was checked" errors from Angular.
        Promise.resolve().then(() => {
            if (this.ngControl) {
                this._value = this.ngControl.value;
            }
            this._setSelectionByValue(this._value);
            this.stateChanges.next();
        });
    }
    /**
     * Sets the selected option based on a value. If no option can be
     * found with the designated value, the select trigger is cleared.
     */
    _setSelectionByValue(value) {
        if (this.multiple && value) {
            if (!Array.isArray(value) && isDevMode()) {
                throw getLySelectNonArrayValueError();
            }
            this._selectionModel.clear();
            value.forEach((currentValue) => this._selectValue(currentValue));
            this._sortValues();
        }
        else {
            this._selectionModel.clear();
            const correspondingOption = this._selectValue(value);
            // Shift focus to the active item. Note that we shouldn't do this in multiple
            // mode, because we don't know what option the user interacted with last.
            if (correspondingOption) {
                this._keyManager.updateActiveItem(correspondingOption);
            }
            else if (!this._opened) {
                // Otherwise reset the highlighted option. Note that we only want to do this while
                // closed, because doing it while open can shift the user's focus unnecessarily.
                this._keyManager.updateActiveItem(-1);
            }
        }
        this._cd.markForCheck();
    }
    /**
     * Finds and selects and option based on its value.
     * @returns Option that has the corresponding value.
     */
    _selectValue(value) {
        const correspondingOption = this.options.find((option) => {
            if (this._valueKey !== same) {
                return this.valueKey(option.value) === this.valueKey(value);
            }
            try {
                // Treat null as a special reset value.
                return option.value != null && this._compareWith(option.value, value);
            }
            catch (error) {
                if (isDevMode()) {
                    // Notify developers of errors in their comparator.
                    console.warn(error);
                }
                return false;
            }
        });
        if (correspondingOption) {
            this._selectionModel.select(correspondingOption);
        }
        return correspondingOption;
    }
    _updatePlacement(needAutoscroll) {
        const hostOverlay = this._overlayRef.containerElement;
        const container = hostOverlay.querySelector('div');
        const triggerFontSize = this._triggerFontSize;
        const { nativeElement } = this.valueTextDivRef;
        let panelWidth;
        if (this.multiple) {
            panelWidth = nativeElement.offsetWidth + triggerFontSize * 4;
        }
        else {
            panelWidth = nativeElement.offsetWidth + triggerFontSize * 2;
        }
        // reset height & width
        this._renderer.setStyle(container, 'height', 'initial');
        this._renderer.setStyle(container, 'width', `${panelWidth}px`);
        let selectedElement = this._selectionModel.isEmpty()
            ? hostOverlay.querySelector('ly-option')
            : this._selectionModel.selected[0]._getHostElement();
        if (!selectedElement) {
            selectedElement = (hostOverlay.firstElementChild.firstElementChild || hostOverlay.firstElementChild);
        }
        const offset = {
            y: -(-(nativeElement.offsetHeight / 2) + selectedElement.offsetTop + (selectedElement.offsetHeight / 2)),
            x: -triggerFontSize
        };
        // scroll to selected option
        if (container.scrollHeight !== container.offsetHeight) {
            if (needAutoscroll) {
                container.scrollTop = container.scrollTop
                    - (container.offsetHeight / 2 - (selectedElement.offsetTop - container.scrollTop)) + selectedElement.offsetHeight / 2;
                offset.y = container.scrollTop + offset.y;
            }
            else {
                offset.y = this._previousOffsetY;
            }
        }
        if (this.multiple) {
            offset.x -= 24;
        }
        this._previousOffsetY = offset.y;
        const position = new LyOverlayPosition(this._theme, this._viewportRuler, nativeElement, hostOverlay)
            .setYAnchor(YPosition.above)
            .setXAnchor(XPosition.before)
            .setXAxis(XPosition.after)
            .setYAxis(YPosition.below)
            .setXOffset(offset.x)
            .setYOffset(offset.y)
            .build();
        if (needAutoscroll) {
            const triggerRect = position.triggerRect;
            const newScrollTop = Math.round((selectedElement.offsetTop
                + (selectedElement.offsetHeight / 2))
                - (triggerRect.top + (triggerRect.height / 2) - position.rawY));
            const scrollTopForOptionAtEnd = (selectedElement.offsetTop + selectedElement.offsetHeight) - container.offsetHeight;
            if (newScrollTop > selectedElement.offsetTop) {
                container.scrollTop = selectedElement.offsetTop;
            }
            else if (scrollTopForOptionAtEnd > newScrollTop) {
                container.scrollTop = scrollTopForOptionAtEnd;
            }
            else {
                container.scrollTop = newScrollTop;
            }
        }
        this._renderer.setStyle(hostOverlay, 'left', `${position.x}px`);
        this._renderer.setStyle(hostOverlay, 'top', `${position.y}px`);
        this._renderer.setStyle(container, 'width', position.width ? `${position.width}px` : `${panelWidth}px`);
        this._renderer.setStyle(container, 'height', position.height ? `${position.height}px` : `100%`);
        this._renderer.setStyle(hostOverlay, 'transform-origin', `${position.xo}px ${position.yo}px 0`);
    }
    /** Sets up a key manager to listen to keyboard events on the overlay panel. */
    _initKeyManager() {
        this._keyManager = new ActiveDescendantKeyManager(this.options)
            .withTypeAhead(this._typeaheadDebounceInterval)
            .withVerticalOrientation()
            .withHorizontalOrientation(this._theme.variables.direction)
            .withHomeAndEnd()
            .withAllowedModifierKeys(['shiftKey']);
        this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => {
            if (this._opened) {
                // Select the active item when tabbing away. This is consistent with how the native
                // select behaves. Note that we only want to do this in single selection mode.
                if (!this.multiple && this._keyManager.activeItem) {
                    this._keyManager.activeItem._selectViaInteraction();
                }
                // Restore focus to the trigger before closing. Ensures that the focus
                // position won't be lost if the user got focus into the overlay.
                this.focus();
                this.close();
            }
        });
        this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
            if (this._opened) {
                this._scrollActiveOptionIntoView();
            }
            else if (!this._opened && !this.multiple && this._keyManager.activeItem) {
                this._keyManager.activeItem._selectViaInteraction();
            }
        });
    }
    /** Sorts the selected values in the selected based on their order in the panel. */
    _sortValues() {
        if (this.multiple) {
            const options = this.options.toArray();
            this._selectionModel.sort((a, b) => {
                return this.sortComparator ? this.sortComparator(a, b, options) :
                    options.indexOf(a) - options.indexOf(b);
            });
            this.stateChanges.next();
        }
    }
    _resetOptions() {
        const changedOrDestroyed = merge(this.options.changes, this._destroy);
        this.optionSelectionChanges.pipe(takeUntil(changedOrDestroyed)).subscribe(event => {
            this._onSelect(event.source, event.isUserInput);
            if (event.isUserInput && !this.multiple && this._opened) {
                this.close();
                this.focus();
            }
        });
    }
    /** Invoked when an option is clicked. */
    _onSelect(option, isUserInput) {
        const wasSelected = this._selectionModel.isSelected(option);
        if (option.value == null && !this._multiple) {
            option.deselect();
            this._selectionModel.clear();
            if (this.value != null) {
                this._propagateChanges(option.value);
            }
        }
        else {
            if (wasSelected !== option.selected) {
                option.selected ? this._selectionModel.select(option) :
                    this._selectionModel.deselect(option);
            }
            if (isUserInput) {
                this._keyManager.setActiveItem(option);
            }
            if (this.multiple) {
                this._sortValues();
                if (isUserInput) {
                    // In case the user selected the option with their mouse, we
                    // want to restore focus back to the trigger, in order to
                    // prevent the select keyboard controls from clashing with
                    // the ones from `ly-option`.
                    this.focus();
                }
            }
        }
        if (wasSelected !== this._selectionModel.isSelected(option)) {
            this._propagateChanges();
        }
        this.stateChanges.next();
    }
    /** Emits change event to set the model value. */
    _propagateChanges(fallbackValue) {
        let valueToEmit = null;
        if (this.multiple) {
            valueToEmit = this.selected.map(option => option.value);
        }
        else {
            valueToEmit = this.selected ? this.selected.value : fallbackValue;
        }
        this._value = valueToEmit;
        this.valueChange.emit(valueToEmit);
        this.onChange(valueToEmit);
        this.selectionChange.emit(new LySelectChange(this, valueToEmit));
        this._cd.markForCheck();
    }
    /**
     * Highlights the selected item. If no option is selected, it will highlight
     * the first item instead.
     */
    _highlightCorrectOption() {
        if (this._keyManager) {
            if (this.empty) {
                this._keyManager.setFirstItemActive();
            }
            else {
                this._keyManager.setActiveItem(this._selectionModel.selected[0]);
            }
        }
    }
    /** Scrolls the active option into view. */
    _scrollActiveOptionIntoView() {
        const el = this._overlayRef.containerElement;
        const container = el.querySelector('div');
        const activeOption = this._keyManager.activeItem._getHostElement();
        // const containerRect = container.getBoundingClientRect();
        // const activeOptionRect = ;
        if (typeof activeOption.scrollIntoView === 'function') {
            if (container.scrollTop > activeOption.offsetTop) {
                container.scrollTop = activeOption.offsetTop;
            }
            else if (container.scrollTop + container.offsetHeight < activeOption.offsetTop + activeOption.offsetHeight) {
                container.scrollTop = activeOption.offsetTop - container.offsetHeight + activeOption.offsetHeight;
            }
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelect, deps: [{ token: i1.LyTheme2 }, { token: i1.StyleRenderer }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i1.LyOverlay }, { token: i2.LyField, optional: true }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i3.ViewportRuler }, { token: i4.NgControl, optional: true, self: true }, { token: i4.NgForm, optional: true }, { token: i4.FormGroupDirective, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.10", type: LySelect, isStandalone: false, selector: "ly-select", inputs: { tabIndex: "tabIndex", typeaheadDebounceInterval: "typeaheadDebounceInterval", value: "value", disabled: "disabled", required: "required", multiple: "multiple", valueKey: "valueKey", placeholder: "placeholder", compareWith: "compareWith", sortComparator: "sortComparator" }, outputs: { openedChange: "openedChange", _openedStream: "opened", _closedStream: "closed", selectionChange: "selectionChange", valueChange: "valueChange" }, host: { listeners: { "keydown": "_handleKeydown($event)", "focus": "_onFocus()", "blur": "_onBlur()" }, properties: { "attr.tabindex": "tabIndex" } }, providers: [
            StyleRenderer,
            { provide: LyFieldControlBase, useExisting: LySelect }
        ], queries: [{ propertyName: "customTrigger", first: true, predicate: LySelectTrigger, descendants: true }, { propertyName: "options", predicate: i0.forwardRef(() => LyOption), descendants: true }], viewQueries: [{ propertyName: "templateRef", first: true, predicate: ["templateRef"], descendants: true }, { propertyName: "valueTextDivRef", first: true, predicate: ["valueText"], descendants: true }, { propertyName: "_options", first: true, predicate: i0.forwardRef(() => LyOption), descendants: true }], exportAs: ["lySelect"], usesInheritance: true, ngImport: i0, template: "<div #valueText [ngSwitch]=\"empty\">\n  <div [className]=\"classes.valueText\" *ngSwitchCase=\"true\">{{ '\\u00A0' }}</div>\n  <div [className]=\"classes.valueText\" *ngSwitchDefault [ngSwitch]=\"!!customTrigger\">\n    <span *ngSwitchDefault>{{ triggerValue || '\\u00A0' }}</span>\n    <ng-content select=\"ly-select-trigger\" *ngSwitchCase=\"true\"></ng-content>\n  </div>\n</div>\n\n<ng-template #templateRef let-getOverlay>\n  <div\n    #container\n    tabindex=\"-1\"\n    role=\"listbox\"\n    [className]=\"classes.container\"\n    [@selectEnter]=\"'in'\"\n    (@selectEnter.done)=\"_panelDoneAnimatingStream.next({state: $event.toState, overlay: getOverlay()})\"\n    (keydown)=\"_handleKeydown($event)\"\n    [style.font-size.px]=\"_triggerFontSize\"\n  >\n    <ng-content></ng-content>\n  </div>\n</ng-template>", dependencies: [{ kind: "directive", type: i5.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i5.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i5.NgSwitchDefault, selector: "[ngSwitchDefault]" }], animations: [...ANIMATIONS], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelect, decorators: [{
            type: Component,
            args: [{ selector: 'ly-select', changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'lySelect', host: {
                        '[attr.tabindex]': 'tabIndex',
                        '(keydown)': '_handleKeydown($event)',
                        '(focus)': '_onFocus()',
                        '(blur)': '_onBlur()',
                    }, animations: [...ANIMATIONS], inputs: ['tabIndex'], providers: [
                        StyleRenderer,
                        { provide: LyFieldControlBase, useExisting: LySelect }
                    ], standalone: false, template: "<div #valueText [ngSwitch]=\"empty\">\n  <div [className]=\"classes.valueText\" *ngSwitchCase=\"true\">{{ '\\u00A0' }}</div>\n  <div [className]=\"classes.valueText\" *ngSwitchDefault [ngSwitch]=\"!!customTrigger\">\n    <span *ngSwitchDefault>{{ triggerValue || '\\u00A0' }}</span>\n    <ng-content select=\"ly-select-trigger\" *ngSwitchCase=\"true\"></ng-content>\n  </div>\n</div>\n\n<ng-template #templateRef let-getOverlay>\n  <div\n    #container\n    tabindex=\"-1\"\n    role=\"listbox\"\n    [className]=\"classes.container\"\n    [@selectEnter]=\"'in'\"\n    (@selectEnter.done)=\"_panelDoneAnimatingStream.next({state: $event.toState, overlay: getOverlay()})\"\n    (keydown)=\"_handleKeydown($event)\"\n    [style.font-size.px]=\"_triggerFontSize\"\n  >\n    <ng-content></ng-content>\n  </div>\n</ng-template>" }]
        }], ctorParameters: () => [{ type: i1.LyTheme2 }, { type: i1.StyleRenderer }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i1.LyOverlay }, { type: i2.LyField, decorators: [{
                    type: Optional
                }] }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i3.ViewportRuler }, { type: i4.NgControl, decorators: [{
                    type: Optional
                }, {
                    type: Self
                }] }, { type: i4.NgForm, decorators: [{
                    type: Optional
                }] }, { type: i4.FormGroupDirective, decorators: [{
                    type: Optional
                }] }], propDecorators: { templateRef: [{
                type: ViewChild,
                args: ['templateRef']
            }], valueTextDivRef: [{
                type: ViewChild,
                args: ['valueText']
            }], _options: [{
                type: ViewChild,
                args: [forwardRef(() => LyOption)]
            }], options: [{
                type: ContentChildren,
                args: [forwardRef(() => LyOption), { descendants: true }]
            }], customTrigger: [{
                type: ContentChild,
                args: [LySelectTrigger]
            }], openedChange: [{
                type: Output
            }], _openedStream: [{
                type: Output,
                args: ['opened']
            }], _closedStream: [{
                type: Output,
                args: ['closed']
            }], selectionChange: [{
                type: Output
            }], valueChange: [{
                type: Output
            }], typeaheadDebounceInterval: [{
                type: Input
            }], value: [{
                type: Input
            }], disabled: [{
                type: Input
            }], required: [{
                type: Input
            }], multiple: [{
                type: Input
            }], valueKey: [{
                type: Input
            }], placeholder: [{
                type: Input
            }], compareWith: [{
                type: Input
            }], sortComparator: [{
                type: Input
            }] } });
/** Event object emitted by LyOption when selected or deselected. */
class LyOptionSelectionChange {
    constructor(
    /** Reference to the option that emitted the event. */
    source, 
    /** Whether the change in the option's value was a result of a user action. */
    isUserInput = false) {
        this.source = source;
        this.isUserInput = isUserInput;
    }
}
/** @docs-private */
class LyOptionBase {
    constructor(_theme, _ngZone, _platform) {
        this._theme = _theme;
        this._ngZone = _ngZone;
        this._platform = _platform;
    }
}
/** @docs-private */
const LyOptionMixinBase = mixinDisableRipple(LyOptionBase);
/**
 * @dynamic
 */
class LyOption extends LyOptionMixinBase {
    _onClick() {
        this._selectViaInteraction();
    }
    /** Whether or not the option is currently selected. */
    get selected() { return this._selected; }
    /** Whether the wrapping component is in multiple selection mode. */
    get multiple() { return this._select && this._select.multiple; }
    /**
     * Tracks simple string values bound to the option element.
     */
    set value(value) {
        this._value = value;
    }
    get value() {
        return this._value;
    }
    /** Whether the option is disabled. */
    set disabled(value) {
        this._disabled = coerceBooleanProperty(value);
    }
    get disabled() {
        return this._disabled;
    }
    /** The displayed value of the option. */
    get viewValue() {
        return (this._getHostElement().textContent || '').trim();
    }
    /** The color of Select option */
    get _color() {
        return this._selected ? this._select._field.color : null;
    }
    /**
     * @deprecated use instead `selected`
     */
    get isSelected() {
        return this._selected;
    }
    constructor(sRenderer, 
    /** @internal */
    _select, _el, 
    /** @internal */
    _rippleService, _renderer, _theme, 
    /** @internal */
    _cd, _ngZone, platform) {
        super(_theme, _ngZone, platform);
        this.sRenderer = sRenderer;
        this._select = _select;
        this._el = _el;
        this._rippleService = _rippleService;
        this._cd = _cd;
        /** @docs-private */
        this.classes = this._theme.addStyleSheet(STYLES, STYLE_PRIORITY);
        this._selected = false;
        this._disabled = false;
        /** Event emitted when the option is selected or deselected. */
        // tslint:disable-next-line: no-output-on-prefix
        this.onSelectionChange = new EventEmitter();
        _renderer.addClass(_el.nativeElement, this.classes.option);
        this._triggerElement = _el;
    }
    ngOnInit() {
        if (this.disableRipple == null) {
            this.disableRipple = DEFAULT_DISABLE_RIPPLE;
        }
    }
    ngOnChanges() { }
    /** Applies the styles for an active item to this item. */
    setActiveStyles() {
        this.sRenderer.addClass(this.classes.optionActive);
    }
    /** Applies the styles for an inactive item to this item. */
    setInactiveStyles() {
        this.sRenderer.removeClass(this.classes.optionActive);
    }
    /** Gets the label to be used when determining whether the option should be focused. */
    getLabel() {
        return this.viewValue;
    }
    /** Selects the option. */
    select() {
        if (!this._selected) {
            this._selected = true;
            this._selectedColor = this._color;
            this._cd.markForCheck();
            this._emitSelectionChangeEvent();
        }
    }
    /** Deselects the option. */
    deselect() {
        if (this._selected) {
            this._selected = false;
            this._selectedColor = null;
            this._cd.markForCheck();
            this._emitSelectionChangeEvent();
        }
    }
    /** Sets focus onto this option. */
    focus(_origin, options) {
        const element = this._getHostElement();
        if (typeof element.focus === 'function') {
            element.focus(options);
        }
    }
    /** Ensures the option is selected when activated from the keyboard. */
    _handleKeydown(event) {
        // tslint:disable-next-line: deprecation
        if ((event.keyCode === ENTER || event.keyCode === SPACE) && !hasModifierKey(event)) {
            this._selectViaInteraction();
            // Prevent the page from scrolling down and form submits.
            event.preventDefault();
        }
    }
    /**
     * `Selects the option while indicating the selection came from the user. Used to
     * determine if the select's view -> model callback should be invoked.`
     */
    _selectViaInteraction() {
        if (!this.disabled) {
            this._selected = this.multiple ? !this._selected : true;
            this._selectedColor = this._color;
            this._cd.markForCheck();
            this._emitSelectionChangeEvent(true);
        }
    }
    /** @internal */
    _getHostElement() {
        return this._el.nativeElement;
    }
    /** Returns the correct tabindex for the option depending on disabled state. */
    _getTabIndex() {
        return this.disabled ? '-1' : '0';
    }
    /** Emits the selection change event. */
    _emitSelectionChangeEvent(isUserInput = false) {
        this.onSelectionChange.emit(new LyOptionSelectionChange(this, isUserInput));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LyOption, deps: [{ token: i1.StyleRenderer }, { token: LySelect }, { token: i0.ElementRef }, { token: i1.LyRippleService }, { token: i0.Renderer2 }, { token: i1.LyTheme2 }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i6.Platform }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.10", type: LyOption, isStandalone: false, selector: "ly-option", inputs: { disableRipple: "disableRipple", value: "value", disabled: "disabled" }, outputs: { onSelectionChange: "onSelectionChange" }, host: { attributes: { "role": "option" }, listeners: { "keydown": "_handleKeydown($event)", "click": "_onClick()" }, properties: { "attr.tabindex": "_getTabIndex()" } }, providers: [
            StyleRenderer
        ], viewQueries: [{ propertyName: "_rippleContainer", first: true, predicate: ["rippleContainer"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<span [className]=\"classes.content\">\n  <ly-checkbox [disabled]=\"disabled\"\n    [color]=\"_color\"\n    [checked]=\"isSelected\"\n    *ngIf=\"_select.multiple\"\n    (click)=\"$event.preventDefault()\"\n  ></ly-checkbox>\n  <span [className]=\"classes.optionText\"><ng-content></ng-content></span>\n</span>\n<div #rippleContainer [className]=\"_rippleService.classes.container\"></div>", dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i7.LyCheckbox, selector: "ly-checkbox", inputs: ["disableRipple", "value", "name", "color", "checked", "indeterminate", "required", "disabled"], outputs: ["change", "indeterminateChange"], exportAs: ["lyCheckbox"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
__decorate([
    Style(value => (theme) => (_className) => `${_className}{color:${theme.colorOf(value)};}`)
], LyOption.prototype, "_selectedColor", void 0);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LyOption, decorators: [{
            type: Component,
            args: [{ selector: 'ly-option', changeDetection: ChangeDetectionStrategy.OnPush, host: {
                        'role': 'option',
                        '(keydown)': '_handleKeydown($event)',
                        '[attr.tabindex]': '_getTabIndex()',
                    }, inputs: [
                        'disableRipple'
                    ], providers: [
                        StyleRenderer
                    ], standalone: false, template: "<span [className]=\"classes.content\">\n  <ly-checkbox [disabled]=\"disabled\"\n    [color]=\"_color\"\n    [checked]=\"isSelected\"\n    *ngIf=\"_select.multiple\"\n    (click)=\"$event.preventDefault()\"\n  ></ly-checkbox>\n  <span [className]=\"classes.optionText\"><ng-content></ng-content></span>\n</span>\n<div #rippleContainer [className]=\"_rippleService.classes.container\"></div>" }]
        }], ctorParameters: () => [{ type: i1.StyleRenderer }, { type: LySelect }, { type: i0.ElementRef }, { type: i1.LyRippleService }, { type: i0.Renderer2 }, { type: i1.LyTheme2 }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i6.Platform }], propDecorators: { _rippleContainer: [{
                type: ViewChild,
                args: ['rippleContainer']
            }], onSelectionChange: [{
                type: Output
            }], _onClick: [{
                type: HostListener,
                args: ['click']
            }], value: [{
                type: Input,
                args: ['value']
            }], disabled: [{
                type: Input
            }], _selectedColor: [] } });
function same(o) {
    return o;
}

class LySelectModule {
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
    static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, declarations: [LySelect, LyOption, LySelectTrigger], imports: [CommonModule,
            LyCommonModule,
            LyCheckboxModule,
            LyOverlayModule], exports: [LySelect, LyOption, LySelectTrigger, LyCommonModule] }); }
    static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, imports: [CommonModule,
            LyCommonModule,
            LyCheckboxModule,
            LyOverlayModule, LyCommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, decorators: [{
            type: NgModule,
            args: [{
                    declarations: [LySelect, LyOption, LySelectTrigger],
                    imports: [
                        CommonModule,
                        LyCommonModule,
                        LyCheckboxModule,
                        LyOverlayModule
                    ],
                    exports: [LySelect, LyOption, LySelectTrigger, LyCommonModule]
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { LyOption, LyOptionBase, LyOptionMixinBase, LyOptionSelectionChange, LySelect, LySelectBase, LySelectChange, LySelectMixinBase, LySelectModule, LySelectTrigger, STYLES };
//# sourceMappingURL=alyle-ui-select.mjs.map