diff --git a/apps/demo-app/src/app/animations/fade.animations.ts b/apps/demo-app/src/app/animations/fade.animations.ts new file mode 100644 index 0000000..520396b --- /dev/null +++ b/apps/demo-app/src/app/animations/fade.animations.ts @@ -0,0 +1,12 @@ +import { animate, AnimationMetadata, style } from '@angular/animations'; + +export const fadeFactory = ( + from: number, + to: number, + duration = '200ms' +): AnimationMetadata[] => { + return [ + style({ opacity: from }), + animate(`${duration} ease`, style({ opacity: to })), + ]; +}; diff --git a/apps/demo-app/src/app/animations/slide.animation.ts b/apps/demo-app/src/app/animations/slide.animation.ts index f1551ce..cc156d9 100644 --- a/apps/demo-app/src/app/animations/slide.animation.ts +++ b/apps/demo-app/src/app/animations/slide.animation.ts @@ -1,6 +1,7 @@ import { animate, AnimationMetadata, + sequence, stagger, style, } from '@angular/animations'; @@ -16,3 +17,22 @@ export const slideFadeAnimationFactory = (): AnimationMetadata[] => { ]), ]; }; + +export const slideDeleteAnimation = (): AnimationMetadata => { + return stagger('80ms', [ + sequence([ + animate( + `200ms cubic-bezier(0.3, 0, 0.8, 0.15)`, + style({ + transform: `translateX(-200%)`, + }) + ), + animate( + `200ms ease`, + style({ + height: 0, + }) + ), + ]), + ]); +}; diff --git a/apps/demo-app/src/app/animations/zoom.animations.ts b/apps/demo-app/src/app/animations/zoom.animations.ts new file mode 100644 index 0000000..003db37 --- /dev/null +++ b/apps/demo-app/src/app/animations/zoom.animations.ts @@ -0,0 +1,15 @@ +import { animate, AnimationMetadata, style } from '@angular/animations'; + +export const zoomFactory = ( + from: number, + to: number, + duration = '500ms' +): AnimationMetadata[] => { + return [ + style({ opacity: from, scale: from }), + animate( + `${duration} cubic-bezier(0.05, 0.7, 0.1, 1)`, + style({ opacity: to, scale: to }) + ), + ]; +}; diff --git a/apps/demo-app/src/app/app.config.ts b/apps/demo-app/src/app/app.config.ts index 7804895..f95c7ed 100644 --- a/apps/demo-app/src/app/app.config.ts +++ b/apps/demo-app/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { ApplicationConfig } from '@angular/core'; +import { ApplicationConfig, importProvidersFrom } from '@angular/core'; import { appRoutes } from './app.routes'; import { provideRouter, @@ -6,6 +6,7 @@ import { withViewTransitions, } from '@angular/router'; import { provideAnimations } from '@angular/platform-browser/animations'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,5 +16,6 @@ export const appConfig: ApplicationConfig = { withComponentInputBinding() ), provideAnimations(), + importProvidersFrom(FontAwesomeModule), ], }; diff --git a/apps/demo-app/src/app/pages/overview/overview.component.html b/apps/demo-app/src/app/pages/overview/overview.component.html index 6759d5f..5cdbc21 100644 --- a/apps/demo-app/src/app/pages/overview/overview.component.html +++ b/apps/demo-app/src/app/pages/overview/overview.component.html @@ -4,6 +4,9 @@ Lorem ipsum dolor... - + Lorem ipsum dolor... diff --git a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.css b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.css index b99341b..69229e0 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.css +++ b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.css @@ -1,33 +1,41 @@ :host { - display: flex; - flex-direction: column; - width: 100%; - margin-top: 0.5rem; + display: flex; + flex-direction: column; + width: 100%; + max-height: 200px; + margin-top: 0.5rem; + overflow-x: auto; } sk-select-option { - background-color: #fff; - color: #000; - padding: 0.5rem 1rem; - cursor: pointer; - transition: color 0.3s ease-in-out, font-weight 0.3s ease-in-out, - font-size 0.3s ease-in-out; + display: flex; + gap: 0.5rem; + background-color: #fff; + color: #000; + padding: 0.5rem 1rem; + cursor: pointer; + transition: color 0.3s ease-in-out, font-weight 0.3s ease-in-out; } sk-select-option:first-child { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; } sk-select-option:last-child { - border-bottom-left-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; } @media (hover) { - sk-select-option:hover { - color: deeppink; - font-weight: bold; - font-size: 1.5em; - } + sk-select-option:hover { + color: var(--sk-primary-color); + font-weight: bold; + } +} + +sk-select-option:focus { + border: none; + color: var(--sk-primary-color); + font-weight: bold; } diff --git a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.html b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.html index f14bade..49199a2 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.html +++ b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.html @@ -1,5 +1,6 @@ -Test 1 -Test 2 -Test 3 -Test 4 -Test 5 +@for (option of options(); track option.value) { + + + {{ option.label }} + +} diff --git a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.spec.ts b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.spec.ts index 57c2fa5..3f06ffa 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.spec.ts +++ b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectOptionsSampleComponent } from './select-options-sample.component'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('SelectOptionsSampleComponent', () => { let component: SelectOptionsSampleComponent; @@ -7,11 +8,14 @@ describe('SelectOptionsSampleComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SelectOptionsSampleComponent], + imports: [SelectOptionsSampleComponent, NoopAnimationsModule], }).compileComponents(); fixture = TestBed.createComponent(SelectOptionsSampleComponent); component = fixture.componentInstance; + const componentRef = fixture.componentRef; + componentRef.setInput('show', true); + componentRef.setInput('options', []); fixture.detectChanges(); }); diff --git a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.ts b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.ts index 25d8bc5..cfa03f6 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.ts +++ b/apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.ts @@ -1,77 +1,16 @@ import { Component, effect, HostBinding, input } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { - animate, - AnimationMetadata, - query, - sequence, - stagger, - style, - transition, - trigger, -} from '@angular/animations'; +import { query, transition, trigger } from '@angular/animations'; import { SelectOptionComponent } from '@qupaya/sketch'; - -export const slideDeleteAnimation = (): AnimationMetadata => { - return stagger('80ms', [ - sequence([ - animate( - `200ms cubic-bezier(0.3, 0, 0.8, 0.15)`, - style({ - transform: `translateX(-200%)`, - }) - ), - animate( - `200ms ease`, - style({ - height: 0, - }) - ), - ]), - ]); -}; - -export const slideFadeAnimationFactory = (): AnimationMetadata[] => { - return [ - style({ opacity: 0, transform: 'translateY(-1.5rem)', scale: 0.8 }), - stagger('32ms', [ - animate( - '150ms cubic-bezier(0.05, 0.7, 0.1, 1)', - style({ opacity: 1, transform: 'translateY(0)', scale: 1 }) - ), - ]), - ]; -}; - -export const fadeFactory = ( - from: number, - to: number, - duration = '200ms' -): AnimationMetadata[] => { - return [ - style({ opacity: from }), - animate(`${duration} ease`, style({ opacity: to })), - ]; -}; - -export const zoomFactory = ( - from: number, - to: number, - duration = '500ms' -): AnimationMetadata[] => { - return [ - style({ opacity: from, scale: from }), - animate( - `${duration} cubic-bezier(0.05, 0.7, 0.1, 1)`, - style({ opacity: to, scale: to }) - ), - ]; -}; +import { slideFadeAnimationFactory } from '../../../animations/slide.animation'; +import { fadeFactory } from '../../../animations/fade.animations'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { SelectDemoOption } from '../select-sample.component'; @Component({ selector: 'app-select-options-sample', standalone: true, - imports: [CommonModule, SelectOptionComponent], + imports: [CommonModule, SelectOptionComponent, FaIconComponent], templateUrl: './select-options-sample.component.html', styleUrl: './select-options-sample.component.css', animations: [ @@ -98,6 +37,7 @@ export const zoomFactory = ( ], }) export class SelectOptionsSampleComponent { + options = input.required(); @HostBinding('@animation') animateOptions = 'hidden'; show = input.required(); diff --git a/apps/demo-app/src/app/pages/select-sample/select-sample.component.css b/apps/demo-app/src/app/pages/select-sample/select-sample.component.css index 65c36b1..3787c7b 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-sample.component.css +++ b/apps/demo-app/src/app/pages/select-sample/select-sample.component.css @@ -1,51 +1,31 @@ sk-select { - display: block; - margin-top: 1rem; + display: block; + margin-top: 1rem; } .sk-label { - padding: 0.5rem 1rem; - border-radius: 8px; - background: rgba(255, 255, 255, 0.1); - color: #fefefe; - box-shadow: rgba(255, 255, 255, 0.05) 0px 6px 24px 0px, rgba(255, 255, 255, 0.08) 0px 0px 0px 1px; -} + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 1rem; + border-radius: 8px; + background: rgb(255 255 255 / 10%); + color: #fefefe; + box-shadow: rgb(255 255 255 / 5%) 0 6px 24px 0, + rgb(255 255 255 / 8%) 0 0 0 1px; -.sk-options { + .sk-label-item { display: flex; - flex-direction: column; + align-items: center; gap: 0.25rem; - width: 100%; - margin-top: 0.5rem; -} + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 90%; -sk-select-option { - background-color: #fff; - color: #000; - padding: 0.5rem 1rem; - cursor: pointer; - transition: color 0.3s ease-in-out, font-weight 0.3s ease-in-out, - font-size 0.3s ease-in-out; -} - -sk-select-option:first-child { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; -} - -sk-select-option:last-child { - border-bottom-left-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; -} - -@media (hover) { - sk-select-option:hover { - color: deeppink; - font-weight: bold; - font-size: 1.5em; + fa-icon { + width: 1.5rem; + height: 1.5rem; } + } } - -sk-select-option:not(:last-child) { - border-bottom: 1px solid #000; -} \ No newline at end of file diff --git a/apps/demo-app/src/app/pages/select-sample/select-sample.component.html b/apps/demo-app/src/app/pages/select-sample/select-sample.component.html index 3c5b938..a4d0b25 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-sample.component.html +++ b/apps/demo-app/src/app/pages/select-sample/select-sample.component.html @@ -1,23 +1,25 @@ Multiple:
Please select an option
- @if (Array.isArray(value)) { - @for (item of value; track item) { - {{ item.data }} +
+ @for (item of selectedValues(); track item.value) { @if + (selectedValues().length <= 1) { + } - } @else { - {{ value?.data }} - } + {{ item.label }} + } +
- + +
diff --git a/apps/demo-app/src/app/pages/select-sample/select-sample.component.spec.ts b/apps/demo-app/src/app/pages/select-sample/select-sample.component.spec.ts index 39ef70e..cb1ebdc 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-sample.component.spec.ts +++ b/apps/demo-app/src/app/pages/select-sample/select-sample.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectSampleComponent } from './select-sample.component'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('SelectSampleComponent', () => { let component: SelectSampleComponent; @@ -7,7 +8,7 @@ describe('SelectSampleComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SelectSampleComponent], + imports: [SelectSampleComponent, NoopAnimationsModule], }).compileComponents(); fixture = TestBed.createComponent(SelectSampleComponent); diff --git a/apps/demo-app/src/app/pages/select-sample/select-sample.component.ts b/apps/demo-app/src/app/pages/select-sample/select-sample.component.ts index 4e0a605..50b73b2 100644 --- a/apps/demo-app/src/app/pages/select-sample/select-sample.component.ts +++ b/apps/demo-app/src/app/pages/select-sample/select-sample.component.ts @@ -1,22 +1,33 @@ -import { Component, effect, signal, untracked } from '@angular/core'; +import { Component, effect, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { - MultipleDirective, - SelectComponent, - SelectOptionComponent, -} from '@qupaya/sketch'; +import { SelectComponent, SelectOptionComponent } from '@qupaya/sketch'; import { FormsModule } from '@angular/forms'; +import { SelectOptionsSampleComponent } from './select-options-sample/select-options-sample.component'; +import { query, transition, trigger } from '@angular/animations'; import { - SelectOptionsSampleComponent, slideDeleteAnimation, slideFadeAnimationFactory, -} from './select-options-sample/select-options-sample.component'; +} from '../../animations/slide.animation'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { - AnimationEvent, - query, - transition, - trigger, -} from '@angular/animations'; + faBell, + faCog, + faEnvelope, + faGlobe, + faHeart, + faHome, + faKey, + faLock, + faStar, + faUser, +} from '@fortawesome/free-solid-svg-icons'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +export interface SelectDemoOption { + label: string; + icon: IconProp; + value: number; +} @Component({ selector: 'app-select-sample', @@ -25,9 +36,9 @@ import { CommonModule, SelectComponent, SelectOptionComponent, - MultipleDirective, FormsModule, SelectOptionsSampleComponent, + FaIconComponent, ], templateUrl: './select-sample.component.html', styleUrl: './select-sample.component.css', @@ -52,27 +63,45 @@ export class SelectSampleComponent { readonly showAnimation = signal(false); readonly animateOptions = signal<'hidden' | 'visible'>('hidden'); readonly multiple = signal(false); - readonly selectedValue = signal< - { data: number } | { data: number }[] | undefined - >(undefined); + readonly selectedValues = signal([]); + readonly options: SelectDemoOption[] = [ + { label: 'Option 1', icon: faHome, value: 1 }, + { label: 'Option 2', icon: faUser, value: 2 }, + { label: 'Option 3', icon: faCog, value: 3 }, + { label: 'Option 4', icon: faHeart, value: 4 }, + { label: 'Option 5', icon: faStar, value: 5 }, + { label: 'Option 6', icon: faBell, value: 6 }, + { label: 'Option 7', icon: faEnvelope, value: 7 }, + { label: 'Option 8', icon: faGlobe, value: 8 }, + { label: 'Option 9', icon: faLock, value: 9 }, + { label: 'Option 10', icon: faKey, value: 10 }, + ]; - value: { data: number } | { data: number }[] | undefined; + value: number | number[] | undefined; switchMultiple(event: Event): void { - if ('checked' in event.target!) { + if (event?.target && 'checked' in event.target) { this.multiple.set(event.target.checked as boolean); } } - logAnimationData(event: AnimationEvent): void { - console.log('animation event', event); + valueChanged(value: number | number[] | undefined): void { + if (!value) { + this.selectedValues.set([]); + } + if (Array.isArray(value)) { + this.selectedValues.set( + this.options.filter((option) => value.includes(option.value)) + ); + } else { + const item = this.options.find((option) => option.value === value); + this.selectedValues.set(item ? [item] : []); + } + this.value = value; } protected readonly runAnimation = effect( - () => { - this.animateOptions.set(this.showAnimation() ? 'visible' : 'hidden'); - console.log('runAnimation', untracked(this.animateOptions)); - }, + () => this.animateOptions.set(this.showAnimation() ? 'visible' : 'hidden'), { allowSignalWrites: true } ); diff --git a/libs/sketch/src/index.ts b/libs/sketch/src/index.ts index 2207f91..2b6503c 100644 --- a/libs/sketch/src/index.ts +++ b/libs/sketch/src/index.ts @@ -1,4 +1,3 @@ export * from './lib/components/sketch/sketch.component'; export * from './lib/components/select'; export * from './lib/components/list'; -export * from './lib/components/sketch/sketch.component'; diff --git a/libs/sketch/src/lib/components/select/components/select-option/select-option.component.css b/libs/sketch/src/lib/components/select/components/select-option/select-option.component.css index e69de29..75396aa 100644 --- a/libs/sketch/src/lib/components/select/components/select-option/select-option.component.css +++ b/libs/sketch/src/lib/components/select/components/select-option/select-option.component.css @@ -0,0 +1,8 @@ +:host { + border: 1px solid white; +} + +:host:focus { + outline: none; + border-color: deeppink; +} diff --git a/libs/sketch/src/lib/components/select/components/select-option/select-option.component.ts b/libs/sketch/src/lib/components/select/components/select-option/select-option.component.ts index 1ec3ecf..9ca549d 100644 --- a/libs/sketch/src/lib/components/select/components/select-option/select-option.component.ts +++ b/libs/sketch/src/lib/components/select/components/select-option/select-option.component.ts @@ -1,5 +1,6 @@ import { Component, + effect, HostBinding, HostListener, inject, @@ -17,21 +18,24 @@ import { SelectComponent } from '../../select.component'; }) export class SelectOptionComponent { private readonly parent = inject(SelectComponent); + private tabIndexValue = 1; value = input.required(); + tabIndex = input(); @HostBinding('tabindex') get tabindex(): number { - return 1; + return this.tabIndexValue; } @HostListener('keydown.space', ['$event']) @HostListener('keydown.enter', ['$event']) - enterItem(): void { - this.parent.selectionChanged(this.value(), true); - } - @HostListener('click', ['$event']) selectItem(): void { this.parent.selectionChanged(this.value()); } + + // WORKAROUND: This is a workaround until the HostBinding can also use as signal + protected readonly updateTabIndex = effect(() => { + this.tabIndexValue = this.tabIndex() ?? 1; + }); } diff --git a/libs/sketch/src/lib/components/select/directives/multiple.directive.ts b/libs/sketch/src/lib/components/select/directives/multiple.directive.ts deleted file mode 100644 index 90ae30c..0000000 --- a/libs/sketch/src/lib/components/select/directives/multiple.directive.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { booleanAttribute, Directive, input } from '@angular/core'; - -@Directive({ - selector: '[skMultiple]', - standalone: true, -}) -export class MultipleDirective { - multiple = input(true, { transform: booleanAttribute, alias: 'skMultiple' }); -} diff --git a/libs/sketch/src/lib/components/select/directives/overlay.directive.ts b/libs/sketch/src/lib/components/select/directives/overlay.directive.ts index 75dfc72..cc8ecd8 100644 --- a/libs/sketch/src/lib/components/select/directives/overlay.directive.ts +++ b/libs/sketch/src/lib/components/select/directives/overlay.directive.ts @@ -4,33 +4,30 @@ import { ElementRef, inject, input, - OnDestroy, - OnInit, output, } from '@angular/core'; import { CdkPortal } from '@angular/cdk/portal'; import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay'; import { DOCUMENT } from '@angular/common'; -import { fromEvent, merge, Subject, takeUntil } from 'rxjs'; +import { fromEvent, merge } from 'rxjs'; +import { toSignal } from '@angular/core/rxjs-interop'; export const DEFAULT_POSITIONS: ConnectedPosition[] = [ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' }, ]; -export const DEFAULT_DROPOUT_POSITIONS: ConnectedPosition[] = [ - { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' }, - { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' }, -]; - @Directive({ selector: '[skCdkOverlay]', standalone: true, }) -export class CdkOverlayDirective implements OnInit, OnDestroy { +export class CdkOverlayDirective { private readonly overlay = inject(Overlay); private readonly elementRef = inject(ElementRef); - private readonly window = inject(DOCUMENT).defaultView; - private readonly destroy$$ = new Subject(); + private readonly window = inject(DOCUMENT)?.defaultView; + + private readonly windowResize = this.window + ? toSignal(fromEvent(this.window, 'resize')) + : undefined; portal = input(undefined, { alias: 'skCdkOverlay' }); showOverlay = input(false, { alias: 'skCdkOverlayShow' }); @@ -41,47 +38,30 @@ export class CdkOverlayDirective implements OnInit, OnDestroy { relativeTo = input(undefined, { alias: 'skCdkOverlayRelativeTo', }); - showChange = output({ alias: 'skCdkOverlayShowChange' }); + backdropClass = input('cdk-overlay-transparent-backdrop', { + alias: 'skCdkOverlayBackdropClass', + }); + visible = output({ alias: 'skCdkOverlayVisible' }); private _overlayRef?: OverlayRef; - private _relatedElement?: HTMLElement; + private _relatedElement?: HTMLElement = + this.relativeTo() || this.elementRef.nativeElement; - protected detectVisibleChange = effect(() => { + protected readonly detectVisibleChange = effect(() => { if (this._relatedElement) { if (this.showOverlay()) { - this.show(); + this.createOverlay(); } else { this.hide(); } } }); - public ngOnInit(): void { - this._relatedElement = this.relativeTo() || this.elementRef.nativeElement; - if (this.window) { - fromEvent(this.window, 'resize') - .pipe(takeUntil(this.destroy$$)) - .subscribe(() => this._syncOverlayWidth()); + protected readonly updateOverlayPortal = effect(() => { + if (this.windowResize && this.windowResize()) { + this.syncOverlayWidth(); } - } - - public ngOnDestroy(): void { - this.hide(); - this.destroy$$.next(); - this.destroy$$.complete(); - } - - private show(): void { - this.createOverlay(); - } - - private hide(): void { - this.showChange.emit(false); - this.window?.setTimeout( - () => this._overlayRef?.dispose(), - this.disposeDelay() - ); - } + }); private createOverlay(): void { if (!this._relatedElement) { @@ -97,14 +77,14 @@ export class CdkOverlayDirective implements OnInit, OnDestroy { this._overlayRef = this.overlay.create({ hasBackdrop: true, - backdropClass: 'cdk-overlay-transparent-backdrop', + backdropClass: this.backdropClass(), positionStrategy, scrollStrategy: this.overlay.scrollStrategies.reposition({ autoClose: true, }), }); - this._syncOverlayWidth(); + this.syncOverlayWidth(); this._overlayRef.attach(this.portal()); merge( @@ -113,7 +93,12 @@ export class CdkOverlayDirective implements OnInit, OnDestroy { ).subscribe(() => this.hide()); } - private _syncOverlayWidth(): void { + private hide(): void { + this.visible.emit(false); + setTimeout(() => this._overlayRef?.dispose(), this.disposeDelay()); + } + + private syncOverlayWidth(): void { this._overlayRef?.updateSize({ width: this._relatedElement?.getBoundingClientRect().width, }); diff --git a/libs/sketch/src/lib/components/select/index.ts b/libs/sketch/src/lib/components/select/index.ts index 8e64083..ad1b0c7 100644 --- a/libs/sketch/src/lib/components/select/index.ts +++ b/libs/sketch/src/lib/components/select/index.ts @@ -1,4 +1,3 @@ export * from './components/select-option/select-option.component'; export * from './select.component'; export * from './directives/overlay.directive'; -export * from './directives/multiple.directive'; diff --git a/libs/sketch/src/lib/components/select/select.component.css b/libs/sketch/src/lib/components/select/select.component.css index e69de29..8533930 100644 --- a/libs/sketch/src/lib/components/select/select.component.css +++ b/libs/sketch/src/lib/components/select/select.component.css @@ -0,0 +1,4 @@ +.sk-select-trigger:focus { + outline: none; + border: 1px solid deeppink; +} diff --git a/libs/sketch/src/lib/components/select/select.component.html b/libs/sketch/src/lib/components/select/select.component.html index 2cff839..a1caaa9 100644 --- a/libs/sketch/src/lib/components/select/select.component.html +++ b/libs/sketch/src/lib/components/select/select.component.html @@ -1,12 +1,13 @@
@if (showPlaceholder()) { @@ -15,8 +16,16 @@ }
+ - +
- +
diff --git a/libs/sketch/src/lib/components/select/select.component.ts b/libs/sketch/src/lib/components/select/select.component.ts index e04f402..399e57b 100644 --- a/libs/sketch/src/lib/components/select/select.component.ts +++ b/libs/sketch/src/lib/components/select/select.component.ts @@ -1,9 +1,8 @@ import { - AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, computed, - contentChildren, effect, forwardRef, inject, @@ -14,12 +13,8 @@ import { ViewEncapsulation, } from '@angular/core'; import { CommonModule, DOCUMENT } from '@angular/common'; -import { - CdkOverlayDirective, - DEFAULT_DROPOUT_POSITIONS, -} from './directives/overlay.directive'; +import { CdkOverlayDirective } from './directives/overlay.directive'; import { CdkPortal } from '@angular/cdk/portal'; -import { MultipleDirective } from './directives/multiple.directive'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { CdkTrapFocus } from '@angular/cdk/a11y'; @@ -30,7 +25,7 @@ import { CdkTrapFocus } from '@angular/cdk/a11y'; providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SelectComponent), + useExisting: forwardRef(() => SelectComponent), multi: true, }, ], @@ -38,14 +33,15 @@ import { CdkTrapFocus } from '@angular/cdk/a11y'; styleUrl: './select.component.css', encapsulation: ViewEncapsulation.ShadowDom, }) -export class SelectComponent implements ControlValueAccessor, AfterViewInit { - private readonly multipleRef = inject(MultipleDirective, { optional: true }); +export class SelectComponent implements ControlValueAccessor { private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly document = inject(DOCUMENT); - readonly animationDelay = input(0); - readonly open = output(); + animationDelay = input(0); + closeOnSelect = input(false); + multiple = input(false, { transform: booleanAttribute }); readonly selectedValue = signal(undefined); + readonly panelIsVisible = signal(false); readonly showPlaceholder = computed(() => { const selectedValue = this.selectedValue(); const isArray = Array.isArray(selectedValue); @@ -53,16 +49,12 @@ export class SelectComponent implements ControlValueAccessor, AfterViewInit { (!isArray && !selectedValue) || (isArray && selectedValue.length <= 0) ); }); - readonly panelIsVisible = signal(false); - readonly options = contentChildren('sk-select-option', { - descendants: true, - }); - readonly overlayPositions = DEFAULT_DROPOUT_POSITIONS; + readonly open = output(); - protected updateSelectionMode = effect( + protected readonly updateSelectionMode = effect( () => { const selectedValue = untracked(this.selectedValue); - if (!this.multipleRef?.multiple()) { + if (!this.multiple()) { this.selectedValue.set( Array.isArray(selectedValue) ? selectedValue[0] : selectedValue ); @@ -84,20 +76,10 @@ export class SelectComponent implements ControlValueAccessor, AfterViewInit { togglePanel(visible: boolean): void { this.panelIsVisible.set(visible); this.open.emit(visible); - - if (visible) { - setTimeout(() => { - this.document.querySelector('sk-select-option')?.focus(); - }, 32); - } - } - - ngAfterViewInit(): void { - console.log('SelectComponent.ngAfterViewInit', this.options()); } - selectionChanged(value: T, forceClose = false): void { - if (this.multipleRef?.multiple()) { + selectionChanged(value: T): void { + if (this.multiple()) { this.selectedValue.update((selected) => { if (Array.isArray(selected)) { return selected.includes(value) @@ -115,11 +97,31 @@ export class SelectComponent implements ControlValueAccessor, AfterViewInit { this.onChange?.(this.selectedValue()); this.onTouched?.(); - if (forceClose) { + if (this.closeOnSelect()) { this.togglePanel(false); } } + keyArrowUp({ target }: Event): void { + console.log('Arrow Down', target); + if (target instanceof HTMLElement) { + console.log('Element', target.previousElementSibling); + if (target.previousElementSibling instanceof HTMLElement) { + target.previousElementSibling.focus(); + } + } + } + + keyArrowDown({ target }: Event): void { + console.log('Arrow Down', target); + if (target instanceof HTMLElement) { + console.log('Element', target.nextElementSibling); + if (target.nextElementSibling instanceof HTMLElement) { + target.nextElementSibling.focus(); + } + } + } + writeValue(obj: T | T[] | undefined): void { this.selectedValue.set(obj); this.changeDetectorRef.markForCheck(); @@ -135,5 +137,4 @@ export class SelectComponent implements ControlValueAccessor, AfterViewInit { private onChange?: (value: T | T[] | undefined) => void; private onTouched?: () => void; - protected readonly Array = Array; } diff --git a/package-lock.json b/package-lock.json index 028d08d..eaed249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,9 @@ "@angular/platform-browser": "~17.3.0", "@angular/platform-browser-dynamic": "~17.3.0", "@angular/router": "~17.3.0", + "@fortawesome/angular-fontawesome": "^0.14.1", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", "@nx/angular": "18.2.2", "playwright": "^1.43.0", "react": "^18.2.0", @@ -5772,6 +5775,51 @@ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, + "node_modules/@fortawesome/angular-fontawesome": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.1.tgz", + "integrity": "sha512-Yb5HLiEOAxjSLEcaOM51CKIrzdfvoDafXVJERm9vufxfZkVZPZJgrZRgqwLVpejgq4/Ez6TqHZ6SqmJwdtRF6g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/package.json b/package.json index 2c7d444..57b8762 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "@angular/platform-browser": "~17.3.0", "@angular/platform-browser-dynamic": "~17.3.0", "@angular/router": "~17.3.0", + "@fortawesome/angular-fontawesome": "^0.14.1", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", "@nx/angular": "18.2.2", "playwright": "^1.43.0", "react": "^18.2.0",