Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions src/modules/display/infrastructure/toast.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
2 changes: 1 addition & 1 deletion src/modules/language/infrastructure/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ function updateModalWhyTranslations(): void {
elements.modalWhyText.textContent = currentTranslations.modalWhyBIP39Text;

elements.modalWhyLink.innerHTML = `
<svg width="18" height="18" style="display: inline-block">
<svg width="18" height="18">
<use href="/sprite.svg#icon-lightbulb"/>
</svg>
${currentTranslations.modalWhyBIP39Link}
Expand Down
47 changes: 5 additions & 42 deletions src/modules/wordInput/infrastructure/wordInput.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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));
});
});