Skip to content
Open
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
26 changes: 19 additions & 7 deletions src/components/__tests__/first-time-tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,32 @@ describe('FirstTimeTooltip', () => {
expect(dismissButton).toHaveAttribute('data-slot', 'button')
})

it('maintains tooltip position with inline styles', async () => {
it('maintains tooltip position with responsive styling', async () => {
mockLocalStorage.getItem.mockReturnValue(null)
render(<FirstTimeTooltip {...defaultProps} />)

await waitFor(() => {
const tooltip = screen.getByTestId('first-time-tooltip')
const container = tooltip.parentElement

// Check the container has proper positioning classes
expect(container).toHaveClass(
'absolute',
'left-0',
'right-0',
'top-full',
'z-50',
'mt-4',
'flex',
'justify-center',
'px-4'
)

// Check the tooltip itself has responsive width styles
expect(tooltip).toHaveStyle({
position: 'absolute',
left: '50%',
top: '100%',
zIndex: 50,
marginTop: '1rem',
width: '20rem',
transform: 'translateX(-50%)',
maxWidth: '100%',
minWidth: '16rem',
})
})
})
Expand Down
3 changes: 2 additions & 1 deletion src/components/__tests__/theme-toggle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@

// Click to cycle to Dark theme
await user.click(button)
await waitFor(() => {

Check failure on line 60 in src/components/__tests__/theme-toggle.test.tsx

View workflow job for this annotation

GitHub Actions / pr-validation

src/components/__tests__/theme-toggle.test.tsx > ThemeToggleAdvanced > cycles through themes when clicked

TypeError: Expected container to be an Element, a Document or a DocumentFragment but got undefined. ❯ checkContainerType node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/helpers.js:61:11 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:86:41 ❯ waitFor node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:36:10 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:54 ❯ Object.asyncWrapper node_modules/.pnpm/@testing-library+react@16.3.0_@testing-library+dom@10.4.1_@types+react-dom@19.1.7_@type_92eb19f4f1da9c4aac97e98556e7995b/node_modules/@testing-library/react/dist/pure.js:88:28 ❯ waitForWrapper node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:35 ❯ src/components/__tests__/theme-toggle.test.tsx:60:11
expect(screen.getByText('Dark')).toBeInTheDocument()
expect(button.getAttribute('aria-label')).toContain('Current theme: Dark')
})
Expand Down Expand Up @@ -92,7 +92,7 @@

// Click to cycle to the next theme
await user.click(button)
await waitFor(() => {

Check failure on line 95 in src/components/__tests__/theme-toggle.test.tsx

View workflow job for this annotation

GitHub Actions / pr-validation

src/components/__tests__/theme-toggle.test.tsx > ThemeToggleAdvanced > cycles through all available themes

TypeError: Expected container to be an Element, a Document or a DocumentFragment but got undefined. ❯ checkContainerType node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/helpers.js:61:11 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:86:41 ❯ waitFor node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:36:10 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:54 ❯ Object.asyncWrapper node_modules/.pnpm/@testing-library+react@16.3.0_@testing-library+dom@10.4.1_@types+react-dom@19.1.7_@type_92eb19f4f1da9c4aac97e98556e7995b/node_modules/@testing-library/react/dist/pure.js:88:28 ❯ waitForWrapper node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:35 ❯ src/components/__tests__/theme-toggle.test.tsx:95:11
expect(button.textContent).not.toBe(initialTheme)
})

Expand Down Expand Up @@ -127,9 +127,10 @@
themes.push(button.textContent)

for (let i = 0; i < 6; i++) {
const previousTheme = button.textContent
await user.click(button)
await waitFor(() => {

Check failure on line 132 in src/components/__tests__/theme-toggle.test.tsx

View workflow job for this annotation

GitHub Actions / pr-validation

src/components/__tests__/theme-toggle.test.tsx > ThemeToggleAdvanced > maintains theme state across multiple cycles

TypeError: Expected container to be an Element, a Document or a DocumentFragment but got undefined. ❯ checkContainerType node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/helpers.js:61:11 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:86:41 ❯ waitFor node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:36:10 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:54 ❯ Object.asyncWrapper node_modules/.pnpm/@testing-library+react@16.3.0_@testing-library+dom@10.4.1_@types+react-dom@19.1.7_@type_92eb19f4f1da9c4aac97e98556e7995b/node_modules/@testing-library/react/dist/pure.js:88:28 ❯ waitForWrapper node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:35 ❯ src/components/__tests__/theme-toggle.test.tsx:132:13
// Wait for theme to change
expect(button.textContent).not.toBe(previousTheme)
})
themes.push(button.textContent)
}
Expand All @@ -151,7 +152,7 @@

// Click to Dark theme
await user.click(button)
await waitFor(() => {

Check failure on line 155 in src/components/__tests__/theme-toggle.test.tsx

View workflow job for this annotation

GitHub Actions / pr-validation

src/components/__tests__/theme-toggle.test.tsx > ThemeToggleAdvanced > displays correct icons for each theme

TypeError: Expected container to be an Element, a Document or a DocumentFragment but got undefined. ❯ checkContainerType node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/helpers.js:61:11 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:86:41 ❯ waitFor node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:36:10 ❯ node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:54 ❯ Object.asyncWrapper node_modules/.pnpm/@testing-library+react@16.3.0_@testing-library+dom@10.4.1_@types+react-dom@19.1.7_@type_92eb19f4f1da9c4aac97e98556e7995b/node_modules/@testing-library/react/dist/pure.js:88:28 ❯ waitForWrapper node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:164:35 ❯ src/components/__tests__/theme-toggle.test.tsx:155:11
expect(screen.getByText('Dark')).toBeInTheDocument()
})

Expand Down
2 changes: 1 addition & 1 deletion src/components/app/reflector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export const Reflector = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<p className="text-muted-foreground mx-auto mb-8 max-w-2xl text-xl sm:text-2xl">
<p className="text-muted-foreground mx-auto mb-8 max-w-2xl text-lg sm:text-xl">
A simple, private space to journal, reflect, and organize your
ideas. No sign-ups, no storage, just you and your thoughts.
</p>
Expand Down
66 changes: 32 additions & 34 deletions src/components/ui/first-time-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,43 +60,41 @@ export function FirstTimeTooltip({
{children}
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.8, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 10 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
style={{
position: 'absolute',
left: '50%',
top: '100%',
zIndex: 50,
marginTop: '1rem',
width: '20rem',
transform: 'translateX(-50%)',
}}
data-testid="first-time-tooltip"
>
<div className="border-border bg-card relative rounded-lg border p-4 shadow-lg">
{/* Arrow */}
<div className="border-border bg-card absolute -top-2 left-1/2 h-4 w-4 -translate-x-1/2 rotate-45 transform border-t border-l"></div>
<div className="absolute top-full right-0 left-0 z-50 mt-4 flex justify-center px-4">
<motion.div
initial={{ opacity: 0, scale: 0.8, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 10 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
style={{
width: '20rem',
maxWidth: '100%',
minWidth: '16rem',
}}
data-testid="first-time-tooltip"
>
<div className="border-border bg-card relative rounded-lg border p-4 shadow-lg">
{/* Arrow */}
<div className="border-border bg-card absolute -top-2 left-1/2 h-4 w-4 -translate-x-1/2 rotate-45 transform border-t border-l"></div>

{/* Content */}
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
className="absolute -top-2 -right-2 h-6 w-6 rounded-full p-0"
data-testid="tooltip-dismiss-button"
>
<X className="h-3 w-3" />
<span className="sr-only">Dismiss tooltip</span>
</Button>
{/* Content */}
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
className="absolute -top-2 -right-2 h-6 w-6 rounded-full p-0"
data-testid="tooltip-dismiss-button"
>
<X className="h-3 w-3" />
<span className="sr-only">Dismiss tooltip</span>
</Button>

<p className="text-foreground pr-4 text-sm">{content}</p>
<p className="text-foreground pr-4 text-sm">{content}</p>
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
Expand Down
78 changes: 69 additions & 9 deletions src/lib/live-regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,32 @@ class LiveRegionManager {

private ensureRegion(priority: AnnouncementPriority): HTMLElement {
if (!this.regions.has(priority)) {
// Ensure DOM is available
if (typeof document === 'undefined') {
// In non-DOM environments, create a minimal mock element
const mockRegion = {
textContent: '',
setAttribute: () => {},
remove: () => {},
className: '',
id: `live-region-${priority}`,
offsetHeight: 0,
} as unknown as HTMLElement
this.regions.set(priority, mockRegion)
return mockRegion
}

const region = document.createElement('div')
region.setAttribute('aria-live', priority)
region.setAttribute('aria-atomic', 'true')
region.setAttribute('role', 'status')
region.className = 'sr-only'
region.id = `live-region-${priority}`

// Add to the document
document.body.appendChild(region)
// Add to the document if body exists
if (document.body) {
document.body.appendChild(region)
}
this.regions.set(priority, region)
}

Expand All @@ -38,7 +55,14 @@ class LiveRegionManager {
private clearTimeout(priority: AnnouncementPriority): void {
const timeoutId = this.timeouts.get(priority)
if (timeoutId) {
window.clearTimeout(timeoutId)
// Handle environments where window.clearTimeout might not be available
if (typeof window !== 'undefined' && window.clearTimeout) {
window.clearTimeout(timeoutId)
} else if (typeof globalThis !== 'undefined' && globalThis.clearTimeout) {
globalThis.clearTimeout(timeoutId)
} else if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutId)
}
this.timeouts.delete(priority)
}
}
Expand Down Expand Up @@ -75,10 +99,31 @@ class LiveRegionManager {

// Auto-clear the message after timeout to prevent accumulation
if (timeout > 0) {
const timeoutId = window.setTimeout(() => {
region.textContent = ''
this.timeouts.delete(priority)
}, timeout)
// Handle environments where window.setTimeout might not be available
let timeoutId: number
if (typeof window !== 'undefined' && window.setTimeout) {
timeoutId = window.setTimeout(() => {
region.textContent = ''
this.timeouts.delete(priority)
}, timeout)
} else if (typeof globalThis !== 'undefined' && globalThis.setTimeout) {
timeoutId = Number(
globalThis.setTimeout(() => {
region.textContent = ''
this.timeouts.delete(priority)
}, timeout)
)
} else if (typeof setTimeout !== 'undefined') {
timeoutId = Number(
setTimeout(() => {
region.textContent = ''
this.timeouts.delete(priority)
}, timeout)
)
} else {
// Fallback: no timeout in environments without timer support
return
}

this.timeouts.set(priority, timeoutId)
}
Expand Down Expand Up @@ -153,7 +198,14 @@ class LiveRegionManager {
})

this.timeouts.forEach((timeoutId) => {
window.clearTimeout(timeoutId)
// Handle environments where window.clearTimeout might not be available
if (typeof window !== 'undefined' && window.clearTimeout) {
window.clearTimeout(timeoutId)
} else if (typeof globalThis !== 'undefined' && globalThis.clearTimeout) {
globalThis.clearTimeout(timeoutId)
} else if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutId)
}
})
this.timeouts.clear()
}
Expand All @@ -165,7 +217,15 @@ class LiveRegionManager {
this.clearAll()

this.regions.forEach((region) => {
region.remove()
try {
if (region.parentNode) {
region.parentNode.removeChild(region)
} else if (region.remove) {
region.remove()
}
} catch {
// Ignore errors if element is already removed or in test environment
}
})

this.regions.clear()
Expand Down
Loading
Loading