diff --git a/src/app/app.html b/src/app/app.html index 310dda3..e21ddc6 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -7,176 +7,15 @@

Switch is: {{ isToggled() ? 'ON' : 'OFF' }}

- @media screen and (max-width: 650px) { - .content { - flex-direction: column; - width: max-content; - } - - .divider { - height: 1px; - width: 100%; - background: var(--red-to-pink-to-purple-horizontal-gradient); - margin-block: 1.5rem; - } - } - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - +
+

Dropdown Component

+ + +

Selected Value: {{ selectedDropdownValue() }}

+
+ diff --git a/src/app/app.ts b/src/app/app.ts index cc9ebb4..398a4d6 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,6 +1,7 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { Switch } from './components/switch/switch'; +import { Dropdown, DropdownOption } from './components/dropdown/dropdown'; @Component({ selector: 'app-root', @@ -9,10 +10,22 @@ import { Switch } from './components/switch/switch'; styleUrl: './app.scss' }) export class App { - protected readonly title = signal('aef-component-test'); isToggled = signal(false); + dropdownOptions: DropdownOption[] = [ + { label: 'Option 1', value: 1 }, + { label: 'Option 2', value: 2 }, + { label: 'Option 3', value: 3 }, + { label: 'Option 4', value: 4 }, + ]; + + selectedDropdownValue = signal(null); + handleToggle(state: boolean) { console.log('Toggle state:', state); } + + handleDropdownChange(value: any) { + console.log('Dropdown value:', value); + } } diff --git a/src/app/components/dropdown/dropdown.html b/src/app/components/dropdown/dropdown.html new file mode 100644 index 0000000..7e38322 --- /dev/null +++ b/src/app/components/dropdown/dropdown.html @@ -0,0 +1,20 @@ + + +@if (isOpen()) { + +} diff --git a/src/app/components/dropdown/dropdown.scss b/src/app/components/dropdown/dropdown.scss new file mode 100644 index 0000000..9701c1d --- /dev/null +++ b/src/app/components/dropdown/dropdown.scss @@ -0,0 +1,116 @@ +@use "sass:list"; +@use "sass:color"; +@use "styles/library" as library; + +$dropdown_bg: #FFFFFF; +$dropdown_border: #E0E0E0; +$dropdown_hover: #F5F5F5; +$dropdown_selected: #FFF4F0; +$dropdown_accent: #FF6E37; + +$dropdown_min_width: 150px; +$dropdown_height: 40px; +$dropdown_padding: (0px, 16px); +$dropdown_border_radius: 8px; + +$dropdown_animation_timing: cubic-bezier(0.4, 0, 0.2, 1); +$dropdown_animation_duration: 0.2s; + +:host { + display: inline-block; + min-width: $dropdown_min_width; + position: relative; + font-family: inherit; + font-size: 14px; + color: #333; + cursor: pointer; + outline: none; + + &:focus-visible { + .dropdown-trigger { + border-color: $dropdown_accent; + box-shadow: 0 0 0 2px rgba($dropdown_accent, 0.2); + } + } +} + +.dropdown-trigger { + display: flex; + align-items: center; + justify-content: space-between; + height: $dropdown_height; + padding: 0 list.nth($dropdown_padding, 2); + background-color: $dropdown_bg; + border: 1px solid $dropdown_border; + border-radius: $dropdown_border_radius; + transition: border-color $dropdown_animation_duration $dropdown_animation_timing, + box-shadow $dropdown_animation_duration $dropdown_animation_timing; + + &:hover { + border-color: color.adjust($dropdown_border, $lightness: -10%); + } + + .placeholder { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .chevron { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #666; + margin-left: 8px; + transition: transform $dropdown_animation_duration $dropdown_animation_timing; + + &.open { + transform: rotate(180deg); + } + } +} + +.dropdown-list { + position: absolute; + top: calc(100% + 4px); + left: 0; + width: 100%; + padding: 4px 0; + margin: 0; + list-style: none; + background-color: $dropdown_bg; + border: 1px solid $dropdown_border; + border-radius: $dropdown_border_radius; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 200px; + overflow-y: auto; + animation: slideIn $dropdown_animation_duration $dropdown_animation_timing; + + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } +} + +.dropdown-item { + padding: 8px list.nth($dropdown_padding, 2); + transition: background-color $dropdown_animation_duration $dropdown_animation_timing; + + &:hover, &.focused { + background-color: $dropdown_hover; + } + + &.selected { + background-color: $dropdown_selected; + color: $dropdown_accent; + font-weight: 600; + } +} diff --git a/src/app/components/dropdown/dropdown.ts b/src/app/components/dropdown/dropdown.ts new file mode 100644 index 0000000..2d64417 --- /dev/null +++ b/src/app/components/dropdown/dropdown.ts @@ -0,0 +1,92 @@ +import { Component, input, model, output, signal, HostListener, ElementRef } from '@angular/core' +import { CommonModule } from '@angular/common' + +export interface DropdownOption { + label: string; + value: any; +} + +@Component({ + selector: 'dropdown', + standalone: true, + imports: [CommonModule], + templateUrl: './dropdown.html', + styleUrl: './dropdown.scss', + host: { + 'role': 'combobox', + 'aria-haspopup': 'listbox', + '[attr.aria-expanded]': 'isOpen()', + '[class.open]': 'isOpen()', + 'tabindex': '0', + '(click)': 'toggle()', + '(keydown.enter)': 'toggle()', + '(keydown.space)': 'onSpaceDown($event)', + '(keydown.escape)': 'close()', + '(keydown.arrowdown)': 'onArrowDown($event)', + '(keydown.arrowup)': 'onArrowUp($event)' + } +}) +export class Dropdown { + options = input([]); + placeholder = input('Select an option'); + selectedValue = model(null); + change = output(); + + isOpen = signal(false); + focusedIndex = signal(-1); + + constructor(private el: ElementRef) {} + + toggle() { + this.isOpen.update(v => !v); + if (this.isOpen()) { + this.focusedIndex.set(this.options().findIndex(o => o.value === this.selectedValue())); + } + } + + close() { + this.isOpen.set(false); + this.focusedIndex.set(-1); + } + + selectOption(option: DropdownOption) { + this.selectedValue.set(option.value); + this.change.emit(option.value); + this.close(); + } + + onSpaceDown(event: Event) { + event.preventDefault(); + this.toggle(); + } + + onArrowDown(event: Event) { + event.preventDefault(); + if (!this.isOpen()) { + this.toggle(); + return; + } + this.focusedIndex.update(i => (i + 1) % this.options().length); + } + + onArrowUp(event: Event) { + event.preventDefault(); + if (!this.isOpen()) { + this.toggle(); + return; + } + this.focusedIndex.update(i => (i - 1 + this.options().length) % this.options().length); + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + if (!this.el.nativeElement.contains(event.target)) { + this.close(); + } + } + + get selectedLabel(): string { + const selected = this.options().find(o => o.value === this.selectedValue()); + return selected ? selected.label : this.placeholder(); + } +}