diff --git a/projects/ng-tw/src/modules/select/select.component.html.bkp b/projects/ng-tw/src/modules/select/select.component.html.bkp
new file mode 100644
index 0000000..b2cae67
--- /dev/null
+++ b/projects/ng-tw/src/modules/select/select.component.html.bkp
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/ng-tw/src/modules/select/select.component.ts b/projects/ng-tw/src/modules/select/select.component.ts
index 71959c2..74d6b1c 100644
--- a/projects/ng-tw/src/modules/select/select.component.ts
+++ b/projects/ng-tw/src/modules/select/select.component.ts
@@ -6,7 +6,6 @@ import {
ContentChildren,
ElementRef,
forwardRef,
- HostListener,
Input,
NgZone,
OnInit,
@@ -16,8 +15,11 @@ import {
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { defer, merge, Observable, startWith, switchMap, take } from 'rxjs';
import { OptionComponent, OptionSelectionChange } from '../option/option.component';
-import { A, DOWN_ARROW, ENTER, hasModifierKey, LEFT_ARROW, RIGHT_ARROW, SPACE, UP_ARROW } from '@angular/cdk/keycodes';
+import { A, DOWN_ARROW, ENTER, hasModifierKey, SPACE, UP_ARROW } from '@angular/cdk/keycodes';
import { ActiveDescendantKeyManager, LiveAnnouncer } from '@angular/cdk/a11y';
+import { difference } from 'lodash';
+import { TwSelectConfig } from './select-config.interface';
+import { TwSelectConfigService } from './select-config.service';
/**
* IDs need to be unique across components, so this counter exists outside of
@@ -31,17 +33,23 @@ let _uniqueIdCounter = 0;
styleUrls: ['./select.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
- role: 'combobox',
- 'aria-autocomplete': 'none',
+ '[attr.tabindex]': '0',
+ '[attr.role]': 'combobox',
+ '[attr.aria-autocomplete]': 'none',
// TODO: the value for aria-haspopup should be `listbox`, but currently it's difficult
// to sync into Google, because of an outdated automated a11y check which flags it as an invalid
// value. At some point we should try to switch it back to being `listbox`.
- 'aria-haspopup': 'true',
+ // '[attr.aria-haspopup]': 'true',
+ '[class]': 'getClasses()',
'[attr.id]': 'id',
'[attr.aria-controls]': 'isOpen ? id + "-panel" : null',
'[attr.aria-expanded]': 'isOpen',
'[attr.aria-disabled]': 'disabled.toString()',
+ '[attr.aria-haspopup]': 'listbox',
+ '[attr.aria-labelledby]': 'listbox-label',
'(keydown)': 'handleKeydown($event)',
+ '(click)': 'openPanel()',
+ cdkOverlayOrigin: '',
},
providers: [
{
@@ -52,6 +60,10 @@ let _uniqueIdCounter = 0;
],
})
export class SelectComponent implements ControlValueAccessor, OnInit, AfterContentInit {
+ @Input() public class: string = '';
+ @Input() public ignore: string = '';
+ @Input() public panelClass: string = '';
+ @Input() public panelIgnoreClass: string = '';
@Input() public placeholder: string = 'Select an option';
@Input() public disabled: boolean = false;
@Input() public id: string = `tw-select-${_uniqueIdCounter++}`;
@@ -78,6 +90,7 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterConte
public overlayWidth!: string;
private _keyManager!: ActiveDescendantKeyManager
;
+ private _config: TwSelectConfig['select'] = this.selectConfig.config.select;
/** Combined stream of all of the child options' change events. */
readonly optionSelectionChanges: Observable = defer(() => {
@@ -96,7 +109,13 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterConte
);
}) as Observable;
- constructor(public cdr: ChangeDetectorRef, private readonly zone: NgZone, private readonly liveAnnouncer: LiveAnnouncer) {}
+ constructor(
+ public cdr: ChangeDetectorRef,
+ public elementRef: ElementRef,
+ private readonly zone: NgZone,
+ private readonly liveAnnouncer: LiveAnnouncer,
+ private readonly selectConfig: TwSelectConfigService
+ ) {}
ngOnInit(): void {}
@@ -313,4 +332,63 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterConte
if (correspondingOption) manager.setActiveItem(correspondingOption);
}
}
+
+ getClasses() {
+ //
+ // Hold classes
+ let classes: string[] = [];
+
+ //
+ // Set global config and classes
+ const config: any = this._config;
+ const globalClasses: string[] = config.host.class ? config.host.class.split(' ').filter((item: string) => item) : [];
+
+ //
+ // Get @input classes if available
+ const inputClasses: string[] = this.class?.split(' ').filter((item: string) => item) || [];
+ const inputIgnoreClasses: string[] = this?.ignore ? this.ignore.split(' ').filter((item: string) => item) : [];
+
+ //
+ // Add global classes
+ classes = [...globalClasses];
+
+ //
+ // Filter global classes using global and @input ignore
+ classes = difference(classes, inputClasses, inputIgnoreClasses);
+
+ return classes?.length ? classes.join(' ') : '';
+ }
+
+ getPanelClass() {
+ //
+ // Hold classes
+ let classes: string[] = [];
+
+ //
+ // Set global config and classes
+ const config: any = this._config;
+ const globalClasses: string[] = config.panel.class ? config.panel.class.split(' ').filter((item: string) => item) : [];
+ const globalMandatoryClasses: string[] = config.panel.mandatoryClass
+ ? config.panel.mandatoryClass.split(' ').filter((item: string) => item)
+ : [];
+
+ //
+ // Get @input classes if available
+ const inputClasses: string[] = this.panelClass?.split(' ').filter((item: string) => item) || [];
+ const inputIgnoreClasses: string[] = this?.panelIgnoreClass ? this.panelIgnoreClass.split(' ').filter((item: string) => item) : [];
+
+ //
+ // Add global classes
+ classes = [...globalClasses];
+
+ //
+ // Filter global classes using global and @input ignore
+ classes = difference(classes, inputClasses, inputIgnoreClasses);
+
+ //
+ // Add mandatory classes
+ classes = [...classes, ...globalMandatoryClasses];
+
+ return classes?.length ? classes.join(' ') : '';
+ }
}
diff --git a/projects/ng-tw/src/modules/select/select.component.ts.bkp b/projects/ng-tw/src/modules/select/select.component.ts.bkp
new file mode 100644
index 0000000..134266e
--- /dev/null
+++ b/projects/ng-tw/src/modules/select/select.component.ts.bkp
@@ -0,0 +1,374 @@
+import {
+ AfterContentInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChildren,
+ ElementRef,
+ forwardRef,
+ HostBinding,
+ HostListener,
+ Input,
+ NgZone,
+ OnInit,
+ QueryList,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { defer, merge, Observable, startWith, switchMap, take } from 'rxjs';
+import { OptionComponent, OptionSelectionChange } from '../option/option.component';
+import { A, DOWN_ARROW, ENTER, hasModifierKey, LEFT_ARROW, RIGHT_ARROW, SPACE, UP_ARROW } from '@angular/cdk/keycodes';
+import { ActiveDescendantKeyManager, LiveAnnouncer } from '@angular/cdk/a11y';
+import { difference } from 'lodash';
+
+/**
+ * IDs need to be unique across components, so this counter exists outside of
+ * the component definition.
+ */
+let _uniqueIdCounter = 0;
+
+@Component({
+ selector: 'tw-select',
+ templateUrl: './select.component.html',
+ styleUrls: ['./select.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ role: 'combobox',
+ 'aria-autocomplete': 'none',
+ // TODO: the value for aria-haspopup should be `listbox`, but currently it's difficult
+ // to sync into Google, because of an outdated automated a11y check which flags it as an invalid
+ // value. At some point we should try to switch it back to being `listbox`.
+ 'aria-haspopup': 'true',
+ '[attr.id]': 'id',
+ '[attr.aria-controls]': 'isOpen ? id + "-panel" : null',
+ '[attr.aria-expanded]': 'isOpen',
+ '[attr.aria-disabled]': 'disabled.toString()',
+ '(keydown)': 'handleKeydown($event)',
+ },
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectComponent),
+ multi: true,
+ },
+ ],
+})
+export class SelectComponent implements ControlValueAccessor, OnInit, AfterContentInit {
+ @Input() public class: string =
+ 'bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm';
+ @Input() public panelClass: string = 'bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5';
+ @Input() public ignore: string = '';
+ @Input() public placeholder: string = 'Select an option';
+ @Input() public disabled: boolean = false;
+ @Input() public id: string = `tw-select-${_uniqueIdCounter++}`;
+ @Input() public compareWith: (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 === o2;
+ @Input()
+ get value(): any {
+ return this.innerValue;
+ }
+ set value(newValue: any) {
+ this.selectOption(newValue, null, false);
+ }
+
+ @ViewChild('trigger', { static: true }) public trigger!: ElementRef;
+ @ContentChildren(OptionComponent, { descendants: true }) public options!: QueryList;
+
+ @HostBinding('class') get classes(): string {
+ console.log('this.class', this.class);
+ //
+ // Set button class
+ this.setButtonClass();
+ // return null cause the host element must be empty of classes
+ return '';
+ }
+
+ public onChange = (value: any) => {};
+ public onTouched = () => {};
+
+ public innerValue: any = null;
+ public htmlValue: any = null;
+
+ public wasTouched: boolean = false;
+ public isOpen: boolean = false;
+ public overlayWidth!: string;
+
+ public _buttonClass: string = '';
+ public _panelClass: string = '';
+ public _panelMandatoryClass: string = 'absolute z-10 mt-1 w-full overflow-auto focus:outline-none sm:text-sm tw-option-panel-scroll';
+
+ private _keyManager!: ActiveDescendantKeyManager;
+
+ /** Combined stream of all of the child options' change events. */
+ readonly optionSelectionChanges: Observable = defer(() => {
+ const options = this.options;
+
+ if (options) {
+ return options.changes.pipe(
+ startWith(options),
+ switchMap(() => merge(...options.map((option) => option.onSelectionChange)))
+ );
+ }
+
+ return this.zone.onStable.pipe(
+ take(1),
+ switchMap(() => this.optionSelectionChanges)
+ );
+ }) as Observable;
+
+ constructor(public cdr: ChangeDetectorRef, private readonly zone: NgZone, private readonly liveAnnouncer: LiveAnnouncer) {
+ //
+ // Set panel class
+ this.setPanelClass();
+ }
+
+ ngOnInit(): void {}
+
+ ngAfterContentInit(): void {
+ this._initKeyManager();
+
+ this.optionSelectionChanges.subscribe((event) => {
+ this.onSelect(event.source, event.isUserInput, event.innerHTML);
+ this.cdr.markForCheck();
+ });
+
+ this.options.changes.pipe(startWith(null)).subscribe(() => {
+ // Defer setting the value in order to avoid the "Expression
+ // has changed after it was checked" errors from Angular.
+ Promise.resolve().then(() => {
+ this.selectOption(this.innerValue, null, false, true);
+ });
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ console.log('changes', changes);
+ //
+ // Change for panelClass
+ if (changes['panelClass']?.previousValue !== changes['panelClass']?.currentValue) {
+ this.setPanelClass();
+ }
+ }
+
+ writeValue(value: any) {
+ this.selectOption(value, null, false);
+ }
+
+ registerOnChange(onChange: any) {
+ this.onChange = onChange;
+ }
+
+ registerOnTouched(onTouched: any) {
+ this.onTouched = onTouched;
+ }
+
+ markAsTouched() {
+ if (!this.wasTouched) {
+ this.onTouched();
+ this.wasTouched = true;
+ }
+ }
+
+ setDisabledState(isDisabled: boolean) {
+ //
+ // Set disabled
+ this.disabled = isDisabled;
+
+ //
+ // Set aria-disabled
+ this.trigger.nativeElement.setAttribute('aria-disabled', isDisabled.toString());
+ }
+
+ openPanel() {
+ //
+ // Validate disabled
+ if (this.disabled === true) return;
+
+ //
+ // open panel
+ this.isOpen = true;
+
+ //
+ // Scroll if we have an active item
+ if (this._keyManager.activeItem) this._keyManager.activeItem.setActiveStylesWithDelay();
+ }
+
+ closePanel() {
+ //
+ // Update manager active item
+ if (this.innerValue) this._updateKeyManagerActiveItem(this.innerValue);
+
+ // close
+ this.isOpen = false;
+ }
+
+ backdropClick() {
+ this.closePanel();
+ }
+
+ selectOption(newValue: any, innerHTML: string | null, touched: boolean, forceUpdate = false) {
+ //
+ // Do nothing if selected is the same as the current value
+ if (this.compareWith(this.innerValue, newValue) && forceUpdate === false) return;
+
+ //
+ // Set new value
+ this.innerValue = newValue;
+
+ //
+ // On change event
+ this.onChange(newValue);
+ // mark as touched if this was made by a user interaction
+ if (touched === true) this.markAsTouched();
+
+ //
+ // Skip if we don't have options
+ if (!this.options) return;
+
+ //
+ // Update content
+ this.updateContent(innerHTML, newValue);
+
+ //
+ // Update manager active item
+ this._updateKeyManagerActiveItem(newValue);
+ }
+
+ onSelect(source: OptionComponent, isUserInput: boolean, innerHTML: string) {
+ //
+ // Validate value is different
+ if (this.innerValue === source.value) return this.closePanel();
+
+ //
+ // Loop options and deselect all except the selected one
+ this.options.forEach((option) => {
+ if (option.id !== source.id) {
+ option.deselect();
+ }
+ });
+
+ //
+ // Select option
+ this.selectOption(source.value, innerHTML, true);
+
+ //
+ // Close
+ this.closePanel();
+ }
+
+ updateContent(newInnerHTML: string | null, value: any) {
+ //
+ // Set innerHTML
+ let innerHTML: string = '';
+
+ //
+ // Get option inner html if not provided
+ // @TODO: find out why this.htmlValue changes from null to '' when we first click on the select trigger, this find runs for each select list at start and we don't need that
+ // possibly we would check and get inside this code only if newInnerHTML is null
+ if (!newInnerHTML) {
+ const option = this.options.find((option) => this.compareWith(option.value, value));
+ innerHTML = option?.contentElement?.nativeElement?.innerHTML || '';
+ }
+ //
+ // innerHTML provided
+ else {
+ innerHTML = newInnerHTML;
+ }
+
+ this.htmlValue = value ? innerHTML : '';
+ }
+
+ handleKeydown(event: KeyboardEvent) {
+ if (!this.disabled) {
+ this.isOpen === true ? this._handleOpenKeydown(event) : this._handleClosedKeydown(event);
+ }
+ }
+
+ private _handleClosedKeydown(event: KeyboardEvent): void {
+ const manager = this._keyManager;
+
+ manager.onKeydown(event);
+ }
+
+ /** Handles keyboard events when the selected is open. */
+ private _handleOpenKeydown(event: KeyboardEvent): void {
+ 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