diff --git a/apps/demo-app/src/app/app.routes.ts b/apps/demo-app/src/app/app.routes.ts index 3a83079..0708c1d 100644 --- a/apps/demo-app/src/app/app.routes.ts +++ b/apps/demo-app/src/app/app.routes.ts @@ -1,4 +1,7 @@ import { Route } from '@angular/router'; +import { SelectSampleComponent } from './pages/select-sample/select-sample.component'; +import { SelectDefaultComponent } from './pages/select-sample/select-default/select-default.component'; +import { SelectWithStyleComponent } from './pages/select-sample/select-with-style/select-with-style.component'; const UUID_REGEX = /^[a-z,0-9,-]{36,36}$/; @@ -17,10 +20,17 @@ export const appRoutes: Route[] = [ }, { path: 'select', - loadComponent: () => - import('./pages/select-sample/select-sample.component').then( - (m) => m.SelectSampleComponent - ), + component: SelectSampleComponent, + children: [ + { + path: '', + component: SelectDefaultComponent, + }, + { + path: 'with-style', + component: SelectWithStyleComponent, + }, + ], }, { path: 'list-sample', 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 5cdbc21..c972fa2 100644 --- a/apps/demo-app/src/app/pages/overview/overview.component.html +++ b/apps/demo-app/src/app/pages/overview/overview.component.html @@ -4,9 +4,6 @@ Lorem ipsum dolor... - + Lorem ipsum dolor... diff --git a/apps/demo-app/src/app/pages/overview/overview.component.ts b/apps/demo-app/src/app/pages/overview/overview.component.ts index 7407495..2d606a4 100644 --- a/apps/demo-app/src/app/pages/overview/overview.component.ts +++ b/apps/demo-app/src/app/pages/overview/overview.component.ts @@ -23,6 +23,17 @@ export class OverviewComponent { }, ]; + readonly selectRoutes = [ + { + route: '/select/with-style', + label: 'Select Sample with styling', + }, + { + route: '/select', + label: 'Select Sample Headless Default', + }, + ]; + readonly widgetLinks = [ { route: '/widget-sample', diff --git a/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.html b/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.html new file mode 100644 index 0000000..0ef48c4 --- /dev/null +++ b/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.html @@ -0,0 +1,9 @@ + + Select option + {{ selectedValue?.label }} + @for (option of options; track option.value) { + + {{ option.label }} + + } + diff --git a/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.ts b/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.ts new file mode 100644 index 0000000..304b6d1 --- /dev/null +++ b/apps/demo-app/src/app/pages/select-sample/select-default/select-default.component.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SelectDemoOption } from '../select-sample.component'; +import { + faBell, + faCog, + faEnvelope, + faGlobe, + faHeart, + faHome, + faKey, + faLock, + faStar, + faUser, +} from '@fortawesome/free-solid-svg-icons'; +import { SelectComponent, SelectOptionComponent } from '@qupaya/sketch'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-select-default', + standalone: true, + imports: [CommonModule, SelectComponent, SelectOptionComponent, FormsModule], + templateUrl: './select-default.component.html', +}) +export class SelectDefaultComponent { + 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 }, + ]; + selectedValue?: SelectDemoOption; +} 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 deleted file mode 100644 index 3787c7b..0000000 --- a/apps/demo-app/src/app/pages/select-sample/select-sample.component.css +++ /dev/null @@ -1,31 +0,0 @@ -sk-select { - display: block; - margin-top: 1rem; -} - -.sk-label { - 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-label-item { - display: flex; - align-items: center; - gap: 0.25rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 90%; - - fa-icon { - width: 1.5rem; - height: 1.5rem; - } - } -} 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 a4d0b25..0680b43 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,25 +1 @@ -Multiple: - - -
Please select an option
-
-
- @for (item of selectedValues(); track item.value) { @if - (selectedValues().length <= 1) { - - } - {{ item.label }} - } -
-
- - - -
+ 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 50b73b2..5f3ea2f 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,27 +1,7 @@ -import { Component, effect, signal } from '@angular/core'; +import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -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 { - slideDeleteAnimation, - slideFadeAnimationFactory, -} from '../../animations/slide.animation'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { - faBell, - faCog, - faEnvelope, - faGlobe, - faHeart, - faHome, - faKey, - faLock, - faStar, - faUser, -} from '@fortawesome/free-solid-svg-icons'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { RouterOutlet } from '@angular/router'; export interface SelectDemoOption { label: string; @@ -32,78 +12,7 @@ export interface SelectDemoOption { @Component({ selector: 'app-select-sample', standalone: true, - imports: [ - CommonModule, - SelectComponent, - SelectOptionComponent, - FormsModule, - SelectOptionsSampleComponent, - FaIconComponent, - ], + imports: [CommonModule, RouterOutlet], templateUrl: './select-sample.component.html', - styleUrl: './select-sample.component.css', - animations: [ - trigger('animation', [ - transition( - 'hidden => visible', - query('.sk-option', slideFadeAnimationFactory(), { - optional: true, - }) - ), - transition( - 'visible => hidden', - query('.sk-option', slideDeleteAnimation(), { - optional: true, - }) - ), - ]), - ], }) -export class SelectSampleComponent { - readonly showAnimation = signal(false); - readonly animateOptions = signal<'hidden' | 'visible'>('hidden'); - readonly multiple = signal(false); - 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: number | number[] | undefined; - - switchMultiple(event: Event): void { - if (event?.target && 'checked' in event.target) { - this.multiple.set(event.target.checked as boolean); - } - } - - 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'), - { allowSignalWrites: true } - ); - - protected readonly Array = Array; -} +export class SelectSampleComponent {} 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-with-style/select-options-sample/select-options-sample.component.css similarity index 100% rename from apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.css rename to apps/demo-app/src/app/pages/select-sample/select-with-style/select-options-sample/select-options-sample.component.css 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-with-style/select-options-sample/select-options-sample.component.html similarity index 100% rename from apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.html rename to apps/demo-app/src/app/pages/select-sample/select-with-style/select-options-sample/select-options-sample.component.html 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-with-style/select-options-sample/select-options-sample.component.spec.ts similarity index 100% rename from apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.spec.ts rename to apps/demo-app/src/app/pages/select-sample/select-with-style/select-options-sample/select-options-sample.component.spec.ts 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-with-style/select-options-sample/select-options-sample.component.ts similarity index 86% rename from apps/demo-app/src/app/pages/select-sample/select-options-sample/select-options-sample.component.ts rename to apps/demo-app/src/app/pages/select-sample/select-with-style/select-options-sample/select-options-sample.component.ts index cfa03f6..f61f5bf 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-with-style/select-options-sample/select-options-sample.component.ts @@ -2,10 +2,10 @@ import { Component, effect, HostBinding, input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { query, transition, trigger } from '@angular/animations'; import { SelectOptionComponent } from '@qupaya/sketch'; -import { slideFadeAnimationFactory } from '../../../animations/slide.animation'; -import { fadeFactory } from '../../../animations/fade.animations'; +import { slideFadeAnimationFactory } from '../../../../animations/slide.animation'; +import { fadeFactory } from '../../../../animations/fade.animations'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { SelectDemoOption } from '../select-sample.component'; +import { SelectDemoOption } from '../../select-sample.component'; @Component({ selector: 'app-select-options-sample', diff --git a/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.css b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.css new file mode 100644 index 0000000..aba0cc1 --- /dev/null +++ b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.css @@ -0,0 +1,46 @@ +sk-select { + --sk-select-label-background: rgb(255 255 255 / 10%); + --sk-select-label-color: #fefefe; + --sk-select-label-padding: 0; + --sk-select-label-border-radius: 8px; + --sk-select-label-border-color: transparent; + --sk-select-label-border-focus-color: deeppink; + + display: block; + margin-top: 1rem; +} + +.sk-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.25rem; + border-radius: 8px; + padding: 0.5rem 1rem; + box-shadow: rgb(255 255 255 / 5%) 0 6px 24px 0, + rgb(255 255 255 / 8%) 0 0 0 1px; +} + +.sk-label .sk-arrow { + transform: rotate(0); + transition: transform 0.3s ease-in-out; +} + +.sk-label.open .sk-arrow { + transform: rotate(180deg); +} + +.sk-label .sk-label-item { + display: flex; + align-items: center; + gap: 0.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 90%; +} + +fa-icon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.html b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.html new file mode 100644 index 0000000..a7def21 --- /dev/null +++ b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.html @@ -0,0 +1,29 @@ +Multiple: + + +
+ Please select an option + +
+
+
+ @for (item of selectedValues(); track item.value) { @if + (selectedValues().length <= 1) { + + } + {{ item.label }} + } +
+ +
+ + + +
diff --git a/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.ts b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.ts new file mode 100644 index 0000000..fdb4c64 --- /dev/null +++ b/apps/demo-app/src/app/pages/select-sample/select-with-style/select-with-style.component.ts @@ -0,0 +1,103 @@ +import { Component, effect, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + faArrowAltCircleDown, + faBell, + faCog, + faEnvelope, + faGlobe, + faHeart, + faHome, + faKey, + faLock, + faStar, + faUser, +} from '@fortawesome/free-solid-svg-icons'; +import { SelectDemoOption } from '../select-sample.component'; +import { query, transition, trigger } from '@angular/animations'; +import { + slideDeleteAnimation, + slideFadeAnimationFactory, +} from '../../../animations/slide.animation'; +import { SelectOptionsSampleComponent } from './select-options-sample/select-options-sample.component'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { SelectComponent } from '@qupaya/sketch'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-select-with-style', + standalone: true, + imports: [ + CommonModule, + SelectOptionsSampleComponent, + FaIconComponent, + SelectComponent, + FormsModule, + ], + templateUrl: './select-with-style.component.html', + styleUrl: './select-with-style.component.css', + animations: [ + trigger('animation', [ + transition( + 'hidden => visible', + query('.sk-option', slideFadeAnimationFactory(), { + optional: true, + }) + ), + transition( + 'visible => hidden', + query('.sk-option', slideDeleteAnimation(), { + optional: true, + }) + ), + ]), + ], +}) +export class SelectWithStyleComponent { + readonly showAnimation = signal(false); + readonly animateOptions = signal<'hidden' | 'visible'>('hidden'); + readonly multiple = signal(false); + 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 }, + ]; + readonly faArrowAltCircleDown = faArrowAltCircleDown; + value: number | number[] | undefined; + + switchMultiple(event: Event): void { + if (event?.target && 'checked' in event.target) { + this.multiple.set(event.target.checked as boolean); + } + } + + 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'), + { allowSignalWrites: true } + ); + + protected readonly Array = Array; +} 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 75396aa..b562e39 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 @@ -1,8 +1,21 @@ :host { - border: 1px solid white; + --sk-select-option-background: white; + --sk-select-option-color: #000; + --sk-select-option-border-color: white; + --sk-select-option-selected-color: deeppink; + --sk-select-option-focus-color: deeppink; + + background: var(--sk-select-option-background); + color: var(--sk-select-option-color); + padding: 0.5rem 1rem; + cursor: pointer; +} + +:host.sk-selected { + color: var(--sk-select-option-selected-color); } :host:focus { outline: none; - border-color: deeppink; + background: rgb(255 255 255 / 60%); } 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 9ca549d..cc77588 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 @@ -5,6 +5,7 @@ import { HostListener, inject, input, + untracked, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SelectComponent } from '../../select.component'; @@ -19,6 +20,7 @@ import { SelectComponent } from '../../select.component'; export class SelectOptionComponent { private readonly parent = inject(SelectComponent); private tabIndexValue = 1; + private selected = false; value = input.required(); tabIndex = input(); @@ -27,6 +29,11 @@ export class SelectOptionComponent { return this.tabIndexValue; } + @HostBinding('class.sk-selected') + get selectedClass(): boolean { + return this.selected; + } + @HostListener('keydown.space', ['$event']) @HostListener('keydown.enter', ['$event']) @HostListener('click', ['$event']) @@ -34,6 +41,23 @@ export class SelectOptionComponent { this.parent.selectionChanged(this.value()); } + // WORKAROUND: This is a workaround until the HostBinding can also use as signal + protected readonly updateSelectedState = effect(() => { + const selectedValue = this.parent.selectedValue(); + const value = untracked(this.value); + + if (!selectedValue) { + this.selected = false; + } else if (Array.isArray(selectedValue)) { + this.selected = selectedValue.includes(value); + } else { + this.selected = selectedValue === value; + } + console.log('selectedValue', selectedValue); + console.log('value', value); + console.log('this.selected', this.selected); + }); + // 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/overlay.directive.ts b/libs/sketch/src/lib/components/select/directives/overlay.directive.ts index cc8ecd8..52eb855 100644 --- a/libs/sketch/src/lib/components/select/directives/overlay.directive.ts +++ b/libs/sketch/src/lib/components/select/directives/overlay.directive.ts @@ -41,6 +41,9 @@ export class CdkOverlayDirective { backdropClass = input('cdk-overlay-transparent-backdrop', { alias: 'skCdkOverlayBackdropClass', }); + panelClass = input('cdk-overlay-panel', { + alias: 'skCdkOverlayPanelClass', + }); visible = output({ alias: 'skCdkOverlayVisible' }); private _overlayRef?: OverlayRef; @@ -73,11 +76,13 @@ export class CdkOverlayDirective { .flexibleConnectedTo(this._relatedElement) .withPositions(this.connectedPositions()) .withPush(true) + .withDefaultOffsetY(10) .withFlexibleDimensions(false); this._overlayRef = this.overlay.create({ hasBackdrop: true, backdropClass: this.backdropClass(), + panelClass: this.panelClass(), positionStrategy, scrollStrategy: this.overlay.scrollStrategies.reposition({ autoClose: true, diff --git a/libs/sketch/src/lib/components/select/select.component.css b/libs/sketch/src/lib/components/select/select.component.css index 8533930..3556b63 100644 --- a/libs/sketch/src/lib/components/select/select.component.css +++ b/libs/sketch/src/lib/components/select/select.component.css @@ -1,4 +1,21 @@ +:host { + --sk-select-label-background: #fff; + --sk-select-label-color: #000; + --sk-select-label-padding: 0.5rem 1rem; + --sk-select-label-border-radius: 8px; + --sk-select-label-border-color: transparent; + --sk-select-label-border-focus-color: deeppink; +} + +.sk-select-trigger { + background: var(--sk-select-label-background); + color: var(--sk-select-label-color); + padding: var(--sk-select-label-padding); + border: 1px solid var(--sk-select-label-border-color); + border-radius: var(--sk-select-label-border-radius); +} + .sk-select-trigger:focus { outline: none; - border: 1px solid deeppink; + border: 1px solid var(--sk-select-label-border-focus-color); } diff --git a/libs/sketch/src/lib/components/select/select.component.html b/libs/sketch/src/lib/components/select/select.component.html index a1caaa9..fa61499 100644 --- a/libs/sketch/src/lib/components/select/select.component.html +++ b/libs/sketch/src/lib/components/select/select.component.html @@ -19,8 +19,7 @@
implements ControlValueAccessor { private readonly changeDetectorRef = inject(ChangeDetectorRef); - private readonly document = inject(DOCUMENT); animationDelay = input(0); closeOnSelect = input(false);