From c9ea8e004cb95fa26e22d25ae13657edd2bbf765 Mon Sep 17 00:00:00 2001 From: JesusValera Date: Sun, 19 Oct 2025 16:42:22 +0200 Subject: [PATCH 1/2] Remove duplicated toast code --- src/modules/display/infrastructure/toast.ts | 50 ++++++++++++------- .../wordInput/infrastructure/wordInput.ts | 47 ++--------------- .../infrastructure/wordInput-keyboard.test.ts | 4 ++ 3 files changed, 40 insertions(+), 61 deletions(-) diff --git a/src/modules/display/infrastructure/toast.ts b/src/modules/display/infrastructure/toast.ts index caf06b2..a650258 100644 --- a/src/modules/display/infrastructure/toast.ts +++ b/src/modules/display/infrastructure/toast.ts @@ -1,8 +1,14 @@ import { currentTranslations } from '../../language'; -let toastTimeout: NodeJS.Timeout | null = null; -let toastRemoveTimeout: NodeJS.Timeout | null = null; -let toastShowTimeout: NodeJS.Timeout | null = null; +const toastTimers = new Map< + string, + { + show: NodeJS.Timeout | null; + hide: NodeJS.Timeout | null; + remove: NodeJS.Timeout | null; + } +>(); + let clickCount = 0; let clickResetTimeout: NodeJS.Timeout | null = null; @@ -18,40 +24,46 @@ export function showDisabledBoxToast(): void { // Show toast only after 2+ clicks if (clickCount >= 2) { - showToast(currentTranslations.disabledBoxMessage); + showToast('toast-notification', currentTranslations.disabledBoxMessage); clickCount = 0; } } -function showToast(message: string): void { - const existingToast = document.getElementById('toast-notification'); +export function showToast(id: string, message: string, duration: number = 3000, className: string = 'toast'): void { + const existingToast = document.getElementById(id); if (existingToast) { existingToast.remove(); } - // Clear all existing timers - if (toastTimeout) clearTimeout(toastTimeout); - if (toastRemoveTimeout) clearTimeout(toastRemoveTimeout); - if (toastShowTimeout) clearTimeout(toastShowTimeout); + const timers = toastTimers.get(id); + if (timers) { + if (timers.show) clearTimeout(timers.show); + if (timers.hide) clearTimeout(timers.hide); + if (timers.remove) clearTimeout(timers.remove); + } + + toastTimers.set(id, { show: null, hide: null, remove: null }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const currentTimers = toastTimers.get(id)!; const toast = document.createElement('div'); - toast.id = 'toast-notification'; - toast.className = 'toast'; + toast.id = id; + toast.className = className; toast.textContent = message; toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'polite'); document.body.appendChild(toast); - // Use setTimeout instead of requestAnimationFrame for better testability - toastShowTimeout = setTimeout(() => { + currentTimers.show = setTimeout(() => { toast.classList.add('show'); - }, 0); + }, 10); - toastTimeout = setTimeout(() => { + currentTimers.hide = setTimeout(() => { toast.classList.remove('show'); - toastRemoveTimeout = setTimeout(() => { + currentTimers.remove = setTimeout(() => { toast.remove(); - }, 300); // Wait for fade out animation - }, 3000); + toastTimers.delete(id); // Clean up timer storage + }, 300); + }, duration); } diff --git a/src/modules/wordInput/infrastructure/wordInput.ts b/src/modules/wordInput/infrastructure/wordInput.ts index 3f928f2..95f2964 100644 --- a/src/modules/wordInput/infrastructure/wordInput.ts +++ b/src/modules/wordInput/infrastructure/wordInput.ts @@ -1,14 +1,11 @@ -import { elements } from '../../bip39'; -import { state, setStateFromIndex, resetBoxes } from '../../bip39'; -import { updateDisplay, setSyncWordInputCallback } from '../../display'; +import { elements, resetBoxes, setStateFromIndex, state } from '../../bip39'; +import { setSyncWordInputCallback, updateDisplay } from '../../display'; +import { showToast } from '../../display'; import { currentTranslations } from '../../language'; -import { isWordInWordlist, getWordIndex, binaryValueToIndex, getWordByIndex } from '../domain/wordInputHelpers'; +import { binaryValueToIndex, getWordByIndex, getWordIndex, isWordInWordlist } from '../domain/wordInputHelpers'; let selectedSuggestionIndex = -1; let hideSuggestionsTimeout: NodeJS.Timeout | null = null; -let invalidToastShowTimeout: NodeJS.Timeout | null = null; -let invalidToastHideTimeout: NodeJS.Timeout | null = null; -let invalidToastRemoveTimeout: NodeJS.Timeout | null = null; export function setupWordInput(): void { // Register callback to avoid circular dependency @@ -52,41 +49,7 @@ function validateWordInput(): void { elements.wordInput.classList.add('error'); resetBoxes(); updateDisplay(); - showInvalidWordToast(); -} - -function showInvalidWordToast(): void { - const existingToast = document.getElementById('invalid-word-toast'); - if (existingToast) { - existingToast.remove(); - } - - // Clear existing timers - if (invalidToastShowTimeout) clearTimeout(invalidToastShowTimeout); - if (invalidToastHideTimeout) clearTimeout(invalidToastHideTimeout); - if (invalidToastRemoveTimeout) clearTimeout(invalidToastRemoveTimeout); - - const toast = document.createElement('div'); - toast.id = 'invalid-word-toast'; - toast.className = 'toast'; - toast.textContent = currentTranslations.invalidWordMessage; - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'polite'); - - document.body.appendChild(toast); - - // Trigger animation - invalidToastShowTimeout = setTimeout(() => { - toast.classList.add('show'); - }, 0); - - // Remove after 3 seconds - invalidToastHideTimeout = setTimeout(() => { - toast.classList.remove('show'); - invalidToastRemoveTimeout = setTimeout(() => { - toast.remove(); - }, 300); - }, 3000); + showToast('invalid-word-toast', currentTranslations.invalidWordMessage); } function handleWordInput(): void { diff --git a/test/integration/wordInput/infrastructure/wordInput-keyboard.test.ts b/test/integration/wordInput/infrastructure/wordInput-keyboard.test.ts index e545f69..7b97235 100644 --- a/test/integration/wordInput/infrastructure/wordInput-keyboard.test.ts +++ b/test/integration/wordInput/infrastructure/wordInput-keyboard.test.ts @@ -1,8 +1,11 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +const mockShowToast = vi.fn(); + vi.mock('../../../../src/modules/display', () => ({ updateDisplay: vi.fn(), setSyncWordInputCallback: vi.fn(), + showToast: mockShowToast, })); const mockElements = { @@ -200,5 +203,6 @@ describe('WordInput - Keyboard Navigation & Suggestions', () => { vi.advanceTimersByTime(300); expect(mockElements.wordInput.classList.add).toHaveBeenCalledWith('error'); expect(resetBoxes).toHaveBeenCalled(); + expect(mockShowToast).toHaveBeenCalledWith('invalid-word-toast', expect.any(String)); }); }); From 8088f49e10b1ce09ec00fd17997a1103e8bece58 Mon Sep 17 00:00:00 2001 From: JesusValera Date: Sun, 19 Oct 2025 17:03:51 +0200 Subject: [PATCH 2/2] Country flag should be rounded --- src/modules/language/infrastructure/language.ts | 2 +- src/styles/components.css | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/language/infrastructure/language.ts b/src/modules/language/infrastructure/language.ts index a4451ed..44bb6ee 100644 --- a/src/modules/language/infrastructure/language.ts +++ b/src/modules/language/infrastructure/language.ts @@ -239,7 +239,7 @@ function updateModalWhyTranslations(): void { elements.modalWhyText.textContent = currentTranslations.modalWhyBIP39Text; elements.modalWhyLink.innerHTML = ` - + ${currentTranslations.modalWhyBIP39Link} diff --git a/src/styles/components.css b/src/styles/components.css index 485a050..704fe59 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -36,6 +36,15 @@ flex-shrink: 0; font-size: 1.5rem; padding: 0; + overflow: hidden; +} + +.language-toggle svg, +#current-flag { + width: 28px; + height: 28px; + border-radius: 50%; + clip-path: circle(50% at center); } .language-toggle:hover {