Skip to content
Closed
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
8 changes: 4 additions & 4 deletions src/components/modal/modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Modal', () => {
it('renders with default props', () => {
const { container } = render(<Modal />)
const modalElement = container.firstChild as HTMLElement
expect(modalElement).toHaveClass('bg-white', 'w-80', 'shadow-4', 'sans-serif', 'relative')
expect(modalElement).toHaveClass('bg-white', 'shadow-4', 'sans-serif', 'relative')
expect(modalElement).toHaveStyle({ maxWidth: '34em' })
})

Expand All @@ -23,7 +23,7 @@ describe('Modal', () => {
const { container } = render(<Modal className={customClass} />)
const modalElement = container.firstChild as HTMLElement
expect(modalElement).toHaveClass(customClass)
expect(modalElement).toHaveClass('bg-white', 'w-80', 'shadow-4', 'sans-serif', 'relative')
expect(modalElement).toHaveClass('bg-white', 'shadow-4', 'sans-serif', 'relative')
})

it('renders children correctly', () => {
Expand All @@ -42,7 +42,7 @@ describe('Modal', () => {

const cancelIcon = container.querySelector('svg')
expect(cancelIcon).toBeInTheDocument()
expect(cancelIcon).toHaveClass('absolute', 'pointer', 'w2', 'h2', 'top-0', 'right-0', 'fill-gray')
expect(cancelIcon).toHaveClass('absolute', 'pointer', 'w2', 'h2', 'fill-gray')
})

it('does not render cancel icon when onCancel is not provided', () => {
Expand Down Expand Up @@ -242,7 +242,7 @@ describe('Modal', () => {
// Check Modal container
const modalElement = container.firstChild as HTMLElement
expect(modalElement).toHaveClass('test-modal')
expect(modalElement).toHaveClass('bg-white', 'w-80', 'shadow-4', 'sans-serif', 'relative')
expect(modalElement).toHaveClass('bg-white', 'shadow-4', 'sans-serif', 'relative')

// Check cancel icon
const cancelIcon = container.querySelector('svg')
Expand Down
10 changes: 4 additions & 6 deletions src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,18 @@ export const Modal: React.FC<ModalProps> = ({
}) => {
return (
<div
className={`${className} bg-white w-80 shadow-4 sans-serif relative`}
className={`${className} bg-white shadow-4 sans-serif relative`}
data-testid="ipfs-modal"
style={{
maxWidth: '34em',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
margin: 'auto'
}}
{...props}
>
{onCancel && (
<CancelIcon
className='absolute pointer w2 h2 top-0 right-0 fill-gray'
className="absolute pointer w2 h2 fill-gray"
style={{ top: '0.5rem', right: '0.5rem' }}
onClick={onCancel}
/>
)}
Expand Down
68 changes: 45 additions & 23 deletions src/components/overlay/overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
import React from 'react'
import { Modal } from 'react-overlays'
import React, { useEffect } from 'react'
import { createPortal } from 'react-dom'

type ModalProps = React.ComponentProps<typeof Modal>

export interface OverlayProps extends Omit<ModalProps, 'renderBackdrop' | 'onHide'> {
export interface OverlayProps {
show: boolean
onLeave: () => void
hidden: boolean
hidden?: boolean
className?: string
children?: React.ReactNode
}

const Overlay: React.FC<OverlayProps> = ({ children, show, onLeave, className = '', hidden, ...props }) => {
const renderBackdrop: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div className='fixed top-0 left-0 right-0 bottom-0 bg-black o-50' hidden={hidden} {...props} />
)
const Overlay: React.FC<OverlayProps> = ({ children, show, onLeave, className = '', hidden }) => {
useEffect(() => {
if (!show) return
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onLeave()
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [show, onLeave])

if (!show) return null

return (
// Note: react-overlays Modal manages its own portal and positioning.
// The Modal child component uses fixed positioning to center itself.
// onHide handles both backdrop clicks and escape key presses.
<Modal
{...props}
show={show}
backdrop={true}
className={`${className} z-max`}
renderBackdrop={renderBackdrop}
onHide={onLeave}>
{children}
</Modal>
const overlay = (
<>
<div
className='fixed top-0 left-0 right-0 bottom-0 bg-black o-50'
hidden={hidden}
Copy link
Copy Markdown
Member

@lidel lidel Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused... when hidden={true}, the backdrop div gets hidden attribute (invisible) but the content container and its children remain fully visible and interactive.

This means the backdrop is invisible but still in the DOM receiving clicks. This seems like unintended behavior? if the overlay is hidden, the whole thing should probably not render at all, no?

(Or maybe I did not look close enough.. anyway.. this feels too complex)

onClick={onLeave}
onKeyDown={(e) => e.key === 'Enter' && onLeave()}
role="button"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The backdrop has role="button", tabIndex={0}, and aria-label="Close modal".

A full-screen invisible button is confusing for screen readers. A backdrop click-to-dismiss is standard UX but shouldn't be announced as a button.

In the future use role="presentation" or just remove the ARIA attributes and keep the click handler.

tabIndex={0}
aria-label="Close modal"
style={{ zIndex: 9998 }}
/>

<div
className={`${className} fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center`}
style={{ zIndex: 9999, pointerEvents: 'none', padding: '2rem' }}
role="dialog"
aria-modal="true"
>
<div style={{ pointerEvents: 'auto', maxWidth: '100%', maxHeight: '100%', overflow: 'auto' }}>
{children}
</div>
</div>
</>
)

return createPortal(overlay, document.body)
}

export default Overlay
1 change: 0 additions & 1 deletion src/diagnostics/logs-screen/buffer-config-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export const BufferConfigModal: React.FC<BufferConfigModalProps> = ({
}

return (
// @ts-expect-error - Overlay is not typed
<Overlay show={isOpen} onLeave={handleCancel}>
<Modal onCancel={handleCancel} className="outline-0">
<div className='pa4'>
Expand Down
1 change: 0 additions & 1 deletion src/diagnostics/logs-screen/log-warning-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const LogWarningModal: React.FC<LogWarningModalProps> = ({
if (warningType == null) return null

return (
// @ts-expect-error - Overlay is not typed
<Overlay show={isOpen} onLeave={onClose}>
<Modal onCancel={onClose} className="outline-0">
<ModalBody>
Expand Down
2 changes: 1 addition & 1 deletion src/files/file-import-status/FileImportStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const FileImportStatus = ({ filesFinished, filesPending, filesErrors, doF
const containerClass = hasErrors ? 'fileImportStatusError' : ''

return (
<div className='fileImportStatus fixed bottom-1 w-100 flex justify-center' style={{ zIndex: 14, pointerEvents: 'none' }}>
<div className='fileImportStatus fixed bottom-1 w-100 flex justify-center' style={{ zIndex: 10000, pointerEvents: 'none' }}>
<div className={`relative br1 dark-gray w-40 center ba b--light-gray bg-white ${containerClass}`} style={{ pointerEvents: 'auto' }}>
<div
tabIndex="0"
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/files.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from './setup/coverage.js'
import { fixtureData } from './fixtures/index.js'
import { files, explore, nav, dismissImportNotification } from './setup/locators.js'
import { files, explore, modal, nav, dismissImportNotification } from './setup/locators.js'
import { selectViewMode, toggleSearchFilter } from '../helpers/grid'
import all from 'it-all'
import filesize from 'filesize'
Expand Down Expand Up @@ -136,8 +136,8 @@ test.describe('Files screen', () => {
await pathInput.fill(testPath)
await files.dialogInput(page, 'name').fill(testFilename)

// Click Import button
const importDialogButton = page.getByRole('button', { name: 'Import' })
// Click Import button in the dialog
const importDialogButton = modal.container(page).getByRole('button', { name: 'Import' })
await expect(importDialogButton).toBeVisible()
await importDialogButton.click()

Expand Down Expand Up @@ -192,7 +192,7 @@ test.describe('Files screen', () => {
await pathInput.fill(nonExistentPath)

// Click Import button to submit
const importDialogButton = page.getByRole('button', { name: 'Import' })
const importDialogButton = modal.container(page).getByRole('button', { name: 'Import' })
await expect(importDialogButton).toBeVisible()
await importDialogButton.click()

Expand Down Expand Up @@ -234,7 +234,7 @@ test.describe('Files screen', () => {
await files.importButton(page).click()
await files.addByPathOption(page).click()
await files.dialogInput(page, 'path').fill(invalidPath)
await page.getByRole('button', { name: 'Import' }).click()
await modal.container(page).getByRole('button', { name: 'Import' }).click()

// Wait for error notification
const notification = page.locator('.fileImportStatus')
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/ipns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ test.describe('IPNS publishing', () => {
await files.dialogInput(page, 'name').fill(testFilename)
await page.keyboard.press('Enter')

// Wait for the dialog to close before proceeding
await expect(pathInput).not.toBeVisible({ timeout: 10000 })

// Also wait for the modal overlay to be completely gone
await page.locator('[aria-label="Close modal"]').waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Silently swallowing test failures masks real bugs. If the modal overlay doesn't close, the test should fail, not silently continue.


// expect file with matching filename to be added to the file list
const fileRow = page.getByTestId('file-row').filter({ hasText: testFilename })
await expect(fileRow).toBeVisible()
Expand Down