From 8d55b478ebf3dfb67fff09147dec56c9ac85f548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4DELTA?= Date: Fri, 27 Mar 2026 14:32:20 +0900 Subject: [PATCH] Fix Animation Consistency --- src/app/components/dropdown/dropdown.scss | 9 ++++-- src/app/components/dropdown/dropdown.ts | 30 +++++++++++++------- src/app/components/switch/switch.scss | 2 +- src/app/components/switch/switch.ts | 3 +- src/app/features/settings/popup.html | 1 + src/app/pages/known-issues/known-issues.html | 6 ++++ 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/app/components/dropdown/dropdown.scss b/src/app/components/dropdown/dropdown.scss index 4eeb180..1c0732b 100644 --- a/src/app/components/dropdown/dropdown.scss +++ b/src/app/components/dropdown/dropdown.scss @@ -27,6 +27,11 @@ $dropdown_animation_duration: 0.2s; cursor: pointer; max-width: 100%; + --dropdown-duration: #{$dropdown_animation_duration}; + + &.reduce-motion { + --dropdown-duration: 0s; + } &:focus-visible { box-shadow: 0 0 0 1px $dropdown_border_color inset, 0 0 0 2px white, 0 0 0 4px $dropdown_accent; @@ -60,7 +65,7 @@ $dropdown_animation_duration: 0.2s; .chevron { width: 24px; height: 24px; flex-shrink: 0; - transition: transform $dropdown_animation_duration $dropdown_animation_timing; + transition: transform var(--dropdown-duration) $dropdown_animation_timing; user-select: none; &.open { @@ -83,7 +88,7 @@ $dropdown_animation_duration: 0.2s; max-height: 240px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - animation: slideIn $dropdown_animation_duration $dropdown_animation_timing; + animation: slideIn var(--dropdown-duration) $dropdown_animation_timing; // 자식 요소의 배경색이 테두리를 침범하지 못하도록 하는 사용자님의 핵심 설계 복구 &::after { diff --git a/src/app/components/dropdown/dropdown.ts b/src/app/components/dropdown/dropdown.ts index 30c4a7f..41dd8a4 100644 --- a/src/app/components/dropdown/dropdown.ts +++ b/src/app/components/dropdown/dropdown.ts @@ -14,21 +14,23 @@ export interface DropdownOption { templateUrl: './dropdown.html', styleUrl: './dropdown.scss', host: { - 'role': 'combobox', + role: 'combobox', 'aria-haspopup': 'listbox', '[attr.aria-expanded]': 'isOpen()', '[attr.aria-controls]': "isOpen() ? dropdownId + '-listbox' : null", - '[attr.aria-activedescendant]': "isOpen() && focusedIndex() >= 0 ? dropdownId + '-option-' + focusedIndex() : null", + '[attr.aria-activedescendant]': + "isOpen() && focusedIndex() >= 0 ? dropdownId + '-option-' + focusedIndex() : null", '[attr.aria-label]': 'ariaLabel()', '[class.open]': 'isOpen()', - 'tabindex': '0', + '[class.reduce-motion]': 'reduceMotion()', + tabindex: '0', '(click)': 'toggle()', '(keydown.enter)': 'onEnter($event)', '(keydown.space)': 'onSpaceDown($event)', '(keydown.escape)': 'close()', '(keydown.arrowdown)': 'onArrowDown($event)', - '(keydown.arrowup)': 'onArrowUp($event)' - } + '(keydown.arrowup)': 'onArrowUp($event)', + }, }) export class Dropdown implements OnInit { options = input>([]); @@ -45,6 +47,12 @@ export class Dropdown implements OnInit { private static nextId = 0; dropdownId = `dropdown-${Dropdown.nextId++}`; + reduceMotion = input( + typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false, + ); + constructor(private el: ElementRef) {} ngOnInit() { @@ -58,7 +66,7 @@ export class Dropdown implements OnInit { } normalizedOptions = computed(() => { - return this.options().map(opt => { + return this.options().map((opt) => { if (typeof opt === 'string') { return { label: opt, value: opt }; } @@ -67,9 +75,9 @@ export class Dropdown implements OnInit { }); toggle() { - this.isOpen.update(v => !v); + this.isOpen.update((v) => !v); if (this.isOpen()) { - this.focusedIndex.set(this.normalizedOptions().findIndex(o => o.value === this.value())); + this.focusedIndex.set(this.normalizedOptions().findIndex((o) => o.value === this.value())); } } @@ -114,7 +122,7 @@ export class Dropdown implements OnInit { this.toggle(); return; } - this.focusedIndex.update(i => { + this.focusedIndex.update((i) => { if (i === -1) return 0; return (i + 1) % this.normalizedOptions().length; }); @@ -126,7 +134,7 @@ export class Dropdown implements OnInit { this.toggle(); return; } - this.focusedIndex.update(i => { + this.focusedIndex.update((i) => { if (i <= 0) return this.normalizedOptions().length - 1; return i - 1; }); @@ -140,7 +148,7 @@ export class Dropdown implements OnInit { } selectedLabel = computed(() => { - const selected = this.normalizedOptions().find(o => o.value === this.value()); + const selected = this.normalizedOptions().find((o) => o.value === this.value()); return selected ? selected.label : this.placeholder(); }); } diff --git a/src/app/components/switch/switch.scss b/src/app/components/switch/switch.scss index 5275822..0c6a4b7 100644 --- a/src/app/components/switch/switch.scss +++ b/src/app/components/switch/switch.scss @@ -12,7 +12,7 @@ $switch_border_radius: 15px; $switch_border_color: oklch(0 0 0 / 20%); $switch_animation_timing: cubic-bezier(0.4, 0, 0.2, 1); -$switch_animation_duration: 0.3s; +$switch_animation_duration: 0.2s; :host { display: inline-block; diff --git a/src/app/components/switch/switch.ts b/src/app/components/switch/switch.ts index f7714c7..2900954 100644 --- a/src/app/components/switch/switch.ts +++ b/src/app/components/switch/switch.ts @@ -18,12 +18,13 @@ import { Component, input, model, output } from '@angular/core'; }) export class Switch { isActive = model(false); + toggle = output(); + reduceMotion = input( typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false, ); - toggle = output(); onClick(): void { this.isActive.update((value) => !value); diff --git a/src/app/features/settings/popup.html b/src/app/features/settings/popup.html index 74d17e8..3f8a3fa 100644 --- a/src/app/features/settings/popup.html +++ b/src/app/features/settings/popup.html @@ -11,6 +11,7 @@
diff --git a/src/app/pages/known-issues/known-issues.html b/src/app/pages/known-issues/known-issues.html index c30697d..c7b7d02 100644 --- a/src/app/pages/known-issues/known-issues.html +++ b/src/app/pages/known-issues/known-issues.html @@ -6,4 +6,10 @@

Dropdown Component를 테스트하기 위한 UI에서, 상위 컨테이너의 크기가 충분하지 않은 경우 드롭다운이 상위 컨테이너 바깥의 공간을 침범하는 문제가 발생했습니다.

Resolved
+ +
+

Issue #2: Dropdown Animation Control

+

Dropdown Component의 애니메이션 효과가 운영체제의 애니메이션 설정에 영향을 받지 않는 문제가 발생했습니다.

+
Resolved
+