Repository URL to install this package:
|
Version:
1.0.0-next.10 ▾
|
/**
* @license
* FOURBURNER CONFIDENTIAL
* Unpublished Copyright (C) 2021 FourBurner Technologies, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains the property of FOURBURNER TECHNOLOGIES,
* INC. The intellectual and technical concepts contained herein are proprietary to FOURBURNER
* TECHNOLOGIES, INC. and may be covered by U.S. and Foreign Patents, patents in process, and are
* protected by trade secret or copyright law. Dissemination of this information or reproduction of
* this material is strictly forbidden unless prior written permission is obtained from FOURBURNER
* TECHNOLOGIES, INC. Access to the source code contained herein is hereby forbidden to anyone
* except current FOURBURNER TECHNOLOGIES, INC. employees, managers or contractors who have executed
* Confidentiality and Non-disclosure agreements explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication or disclosure of
* this source code, which includes information that is confidential and/or proprietary, and is a
* trade secret, of FOURBURNER TECHNOLOGIES, INC. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
* PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS
* WRITTEN CONSENT OF FOURBURNER TECHNOLOGIES, INC. IS STRICTLY PROHIBITED, AND IN VIOLATION OF
* APPLICABLE LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR
* RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS
* CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {ENTER, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
import {
AfterViewChecked,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Inject,
Input,
OnDestroy,
Optional,
Output,
QueryList,
ViewEncapsulation,
Directive,
} from '@angular/core';
import {FocusOptions, FocusableOption, FocusOrigin} from '@angular/cdk/a11y';
import {Subject} from 'rxjs';
import {TWNDOptgroup, _TWNDOptgroupBase, TWND_OPTGROUP} from './optgroup';
import {TWNDOptionParentComponent, TWND_OPTION_PARENT_COMPONENT} from './option.parent';
/**
* Option IDs need to be unique across components, so this counter exists outside of
* the component definition.
*/
let _uniqueIdCounter = 0;
/** Event object emitted by TWNDOption when selected or deselected. */
export class TWNDOptionSelectionChange
{
constructor(
/** Reference to the option that emitted the event. */
public source: _TWNDOptionBase,
/** Whether the change in the option's value was a result of a user action. */
public isUserInput = false,
)
{}
}
@Directive() export class _TWNDOptionBase implements FocusableOption, AfterViewChecked, OnDestroy
{
private _selected = false;
private _active = false;
private _disabled = false;
private _mostRecentViewValue = '';
/** Whether the wrapping component is in multiple selection mode. */
get multiple()
{
return this._parent && this._parent.multiple;
}
/** Whether or not the option is currently selected. */
get selected(): boolean
{
return this._selected;
}
/** The form value of the option. */
@Input() value: any;
/** The unique ID of the option. */
@Input() id: string = `twnd-option-${_uniqueIdCounter++}`;
/** Whether the option is disabled. */
@Input() get disabled()
{
return (this.group && this.group.disabled) || this._disabled;
}
set disabled(value: any)
{
this._disabled = coerceBooleanProperty(value);
}
/** Whether ripples for the option are disabled. */
get disableRipple()
{
return this._parent && this._parent.disableRipple;
}
/** Event emitted when the option is selected or deselected. */
// tslint:disable-next-line:no-output-on-prefix
@Output() readonly onSelectionChange = new EventEmitter<TWNDOptionSelectionChange>();
/** Emits when the state of the option changes and any parents have to be notified. */
readonly _stateChanges = new Subject<void>();
constructor(
private _element: ElementRef<HTMLElement>,
private _changeDetectorRef: ChangeDetectorRef,
private _parent: TWNDOptionParentComponent,
readonly group: _TWNDOptgroupBase,
)
{}
/**
* Whether or not the option is currently active and ready to be selected.
* An active option displays styles as if it is focused, but the
* focus is actually retained somewhere else. This comes in handy
* for components like autocomplete where focus must remain on the input.
*/
get active(): boolean
{
return this._active;
}
/**
* The displayed value of the option. It is necessary to show the selected option in the
* select's trigger.
*/
get viewValue(): string
{
// TODO(kara): Add input property alternative for node envs.
return (this._getHostElement().textContent || '').trim();
}
/** Selects the option. */
select(): void
{
if (!this._selected) {
this._selected = true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Deselects the option. */
deselect(): void
{
if (this._selected) {
this._selected = false;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Sets focus onto this option. */
focus(_origin?: FocusOrigin, options?: FocusOptions): void
{
// Note that we aren't using `_origin`, but we need to keep it because some internal consumers
// use `TWNDOption` in a `FocusKeyManager` and we need it to match `FocusableOption`.
const element = this._getHostElement();
if (typeof element.focus === 'function') {
element.focus(options);
}
}
/**
* This method sets display styles on the option to make it appear
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setActiveStyles(): void
{
if (!this._active) {
this._active = true;
this._changeDetectorRef.markForCheck();
}
}
/**
* This method removes display styles on the option that made it appear
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setInactiveStyles(): void
{
if (this._active) {
this._active = false;
this._changeDetectorRef.markForCheck();
}
}
/** Gets the label to be used when determining whether the option should be focused. */
getLabel(): string
{
return this.viewValue;
}
/** Ensures the option is selected when activated from the keyboard. */
_handleKeydown(event: KeyboardEvent): void
{
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(): void
{
if (!this.disabled) {
this._selected = this.multiple ? !this._selected : true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent(true);
}
}
/**
* Gets the `aria-selected` value for the option. We explicitly omit the `aria-selected`
* attribute from single-selection, unselected options. Including the `aria-selected="false"`
* attributes adds a significant amount of noise to screen-reader users without providing useful
* information.
*/
_getAriaSelected(): boolean|null
{
return this.selected || (this.multiple ? false : null);
}
/** Returns the correct tabindex for the option depending on disabled state. */
_getTabIndex(): string
{
return this.disabled ? '-1' : '0';
}
/** Gets the host DOM element. */
_getHostElement(): HTMLElement
{
return this._element.nativeElement;
}
ngAfterViewChecked()
{
// Since parent components could be using the option's label to display the selected values
// (e.g. `twnd-select`) and they don't have a way of knowing if the option's label has changed
// we have to check for changes in the DOM ourselves and dispatch an event. These checks are
// relatively cheap, however we still limit them only to selected options in order to avoid
// hitting the DOM too often.
if (this._selected) {
const viewValue = this.viewValue;
if (viewValue !== this._mostRecentViewValue) {
this._mostRecentViewValue = viewValue;
this._stateChanges.next();
}
}
}
ngOnDestroy()
{
this._stateChanges.complete();
}
/** Emits the selection change event. */
private _emitSelectionChangeEvent(isUserInput = false): void
{
this.onSelectionChange.emit(new TWNDOptionSelectionChange(this, isUserInput));
}
static ngAcceptInputType_disabled: BooleanInput;
}
/**
* Single option inside of a `<twnd-select>` element.
*/
/* clang-format off */
@Component({
selector : 'twnd-option',
exportAs : 'twndOption',
host : {
'role' : 'option',
'[attr.tabindex]' : '_getTabIndex()',
'[class.twnd-selected]' : 'selected',
'[class.twnd-option-multiple]' : 'multiple',
'[class.twnd-active]' : 'active',
'[id]' : 'id',
'[attr.aria-selected]' : '_getAriaSelected()',
'[attr.aria-disabled]' : 'disabled.toString()',
'[class.twnd-option-disabled]' : 'disabled',
'(click)' : '_selectViaInteraction()',
'(keydown)' : '_handleKeydown($event)',
'class' : 'twnd-option twnd-focus-indicator',
},
templateUrl : 'option.html',
encapsulation : ViewEncapsulation.None,
changeDetection : ChangeDetectionStrategy.OnPush,
})
/* clang-format on */
export class TWNDOption extends _TWNDOptionBase
{
constructor(
element: ElementRef<HTMLElement>,
changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(TWND_OPTION_PARENT_COMPONENT) parent: TWNDOptionParentComponent,
@Optional() @Inject(TWND_OPTGROUP) group: TWNDOptgroup,
)
{
super(element, changeDetectorRef, parent, group);
}
}
/**
* Counts the amount of option group labels that precede the specified option.
* @param optionIndex Index of the option at which to start counting.
* @param options Flat list of all of the options.
* @param optionGroups Flat list of all of the option groups.
* @docs-private
*/
export function _countGroupLabelsBeforeOption(
optionIndex: number,
options: QueryList<TWNDOption>,
optionGroups: QueryList<TWNDOptgroup>,
): number
{
if (optionGroups.length) {
let optionsArray = options.toArray();
let groups = optionGroups.toArray();
let groupCounter = 0;
for (let i = 0; i < optionIndex + 1; i++) {
if (optionsArray[i].group && optionsArray[i].group === groups[groupCounter]) {
groupCounter++;
}
}
return groupCounter;
}
return 0;
}
/**
* Determines the position to which to scroll a panel in order for an option to be into view.
* @param optionOffset Offset of the option from the top of the panel.
* @param optionHeight Height of the options.
* @param currentScrollPosition Current scroll position of the panel.
* @param panelHeight Height of the panel.
* @docs-private
*/
export function _getOptionScrollPosition(
optionOffset: number,
optionHeight: number,
currentScrollPosition: number,
panelHeight: number,
): number
{
if (optionOffset < currentScrollPosition) {
return optionOffset;
}
if (optionOffset + optionHeight > currentScrollPosition + panelHeight) {
return Math.max(0, optionOffset - panelHeight + optionHeight);
}
return currentScrollPosition;
}