Restructure 7 modal components to use 3-layer architecture preventing native select dropdown menus from being blocked by modal overlays. Components fixed: - ProxyHostForm: ACL selector and Security Headers dropdowns - User management: Role and permission mode selection - Uptime monitors: Monitor type selection (HTTP/TCP) - Remote servers: Provider selection dropdown - CrowdSec: IP ban duration selection The fix separates modal background overlay (z-40) from form container (z-50) and enables pointer events only on form content, allowing native dropdown menus to render above all modal layers. Resolves user inability to select security policies, user roles, monitor types, and other critical configuration options through the UI interface.
32 KiB
Modal Z-Index Fix Implementation Plan
Date: 2026-02-04
Issue: Modal overlay z-index conflicts preventing dropdown interactions
Scope: 7 P0 critical modal components with native <select> elements
Estimated Time: 6-8 hours total implementation + testing
REQUIREMENTS (EARS Format)
WHEN a user opens any modal with dropdown elements, THE SYSTEM SHALL allow dropdown interaction without z-index conflicts
WHEN a user clicks on native select elements in modals, THE SYSTEM SHALL display dropdown options and allow selection
WHEN a user clicks outside modal content, THE SYSTEM SHALL close the modal (preserve existing behavior)
WHEN a user presses the Escape key, THE SYSTEM SHALL close the modal (preserve existing behavior)
TECHNICAL DESIGN
Root Cause Analysis
All affected modals use the same problematic pattern:
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card ...">
<form>
<select> {/* BROKEN: Dropdown can't render above z-50 overlay */} </select>
</form>
</div>
</div>
Solution: 3-Layer Modal Architecture
Replace single-layer pattern with 3 distinct layers:
{/* FIXED: 3-layer architecture */}
<>
{/* Layer 1: Background overlay (z-40) - Click to close */}
<div className="fixed inset-0 bg-black/50 z-40" onClick={onCancel} />
{/* Layer 2: Container (z-50, pointer-events-none) - Centers content */}
<div className="fixed inset-0 flex items-center justify-center p-4 pointer-events-none z-50">
{/* Layer 3: Content (pointer-events-auto) - Interactive form */}
<div className="bg-dark-card ... pointer-events-auto">
<form className="pointer-events-auto">
<select> {/* WORKS: Dropdown renders above all layers */} </select>
</form>
</div>
</div>
</>
Key CSS Classes:
pointer-events-noneon container: Passes clicks through to overlaypointer-events-autoon content: Re-enables form interactions- Background overlay at
z-40: Lower than form container atz-50
- Native Dropdown Conflict: Native HTML
<select>elements spawn dropdown menus outside the normal DOM flow - Z-Index Stacking Problem: The dropdown menu may render at a z-index below the parent overlay's z-50, making it invisible or unclickable
- Pointer Events: The overlay's
fixed inset-0may block pointer events from reaching child dropdowns depending on browser implementation
Evidence from Related Components:
The ConfigReloadOverlay component in frontend/src/components/LoadingStates.tsx uses an identical problematic pattern:
<div className="fixed inset-0 bg-slate-900/70 backdrop-blur-sm flex items-center justify-center z-50">
{/* Content */}
</div>
This is so well-known as a problem that test helpers document workarounds for it:
// ✅ FIX P0: Wait for ConfigReloadOverlay to disappear before clicking
// The ConfigReloadOverlay component (z-50) intercepts pointer events
// during Caddy config reloads, blocking all interactions
const overlay = page.locator('[data-testid="config-reload-overlay"]');
await overlay.waitFor({ state: 'hidden', timeout: 10000 });
CHANGELOG.md Reference:
- Line 75: "E2E Tests: Added
data-testid="config-reload-overlay"toConfigReloadOverlaycomponent"
This confirms the pattern is known to cause interaction problems.
Secondary Issues
Issue 2: Native Select Elements vs Modal Z-Index Conflict
Affected Components:
ACL Dropdown (frontend/src/components/AccessListSelector.tsx):
<select
value={value || 0}
onChange={(e) => onChange(parseInt(e.target.value) || null)}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>No Access Control (Public)</option>
{accessLists?.filter((acl) => acl.enabled).map((acl) => (
<option key={acl.id} value={acl.id}>
{acl.name} ({acl.type.replace('_', ' ')})
</option>
))}
</select>
Security Headers Dropdown (frontend/src/components/ProxyHostForm.tsx):
<select
value={formData.security_header_profile_id || 0}
onChange={e => {
const value = e.target.value === "0" ? null : parseInt(e.target.value) || null
setFormData({ ...formData, security_header_profile_id: value })
}}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>None (No Security Headers)</option>
<optgroup label="Quick Presets">
{securityProfiles?.filter(p => p.is_preset)
.sort((a, b) => a.security_score - b.security_score)
.map(profile => (
<option key={profile.id} value={profile.id}>
{profile.name} (Score: {profile.security_score}/100)
</option>
))}
</optgroup>
{/* ... additional profiles ... */}
</select>
Problems:
- Native
<select>elements attempt to render dropdown menus at the browser's native overlay layer - These native menus may be rendered at z-index below the parent modal's
z-50 - No explicit z-index management for the dropdown menus
- No
pointer-events-autodeclaration to explicitly allow click propagation
Issue 3: Missing Pointer-Events Cascade
Problem: Form elements don't have explicit pointer-events-auto to override potential parent restrictions.
Related Code (frontend/src/components/ProxyHostForm.tsx):
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Dropdowns here - but no pointer-events-auto */}
</form>
Component Architecture & Event Handler Analysis
ACL Dropdown (AccessListSelector Component)
File: frontend/src/components/AccessListSelector.tsx
Component Structure:
interface AccessListSelectorProps {
value: number | null;
onChange: (id: number | null) => void;
}
export default function AccessListSelector({ value, onChange }: AccessListSelectorProps) {
const { data: accessLists } = useAccessLists();
const selectedACL = accessLists?.find((acl) => acl.id === value);
return (
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Access Control List
<span className="text-gray-500 font-normal ml-2">(Optional)</span>
</label>
<select
value={value || 0}
onChange={(e) => onChange(parseInt(e.target.value) || null)} // ✅ Handler is defined
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>No Access Control (Public)</option>
{accessLists?.filter((acl) => acl.enabled).map((acl) => (
<option key={acl.id} value={acl.id}>
{acl.name} ({acl.type.replace('_', ' ')})
</option>
))}
</select>
{/* ... display selected ACL details ... */}
</div>
);
}
Event Handler Analysis:
- ✅
onChangehandler is correctly defined - ✅ Handler parses the selected value correctly
- ✅ Handler calls parent's
onChangeprop with the correct value - ❌ THE PROBLEM: Parent modal overlay prevents click event from reaching the
<select>element in the first place
State Management Flow:
AccessListSelector
↓ onChange={(e) => onChange(...)}
↓
ProxyHostForm (parent)
↓ onChange={(id) => setFormData({...formData, access_list_id: id})}
↓
ProxyHosts page (grandparent)
↓ handleSubmit() → updateHost()/createHost()
State Flow Status: ✅ Correct - No issues with state management or event handlers
Security Headers Dropdown (ProxyHostForm Component)
File: frontend/src/components/ProxyHostForm.tsx
Component Structure:
export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) {
const [formData, setFormData] = useState<ProxyHostFormState>({
security_header_profile_id: host?.security_header_profile_id || null,
// ... other fields
});
const { data: securityProfiles } = useSecurityHeaderProfiles();
return (
// ... modal structure ...
<select
value={formData.security_header_profile_id || 0}
onChange={e => {
const value = e.target.value === "0" ? null : parseInt(e.target.value) || null
setFormData({ ...formData, security_header_profile_id: value }) // ✅ Handler is defined
}}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>None (No Security Headers)</option>
<optgroup label="Quick Presets">
{securityProfiles?.filter(p => p.is_preset)
.sort((a, b) => a.security_score - b.security_score)
.map(profile => (
<option key={profile.id} value={profile.id}>
{profile.name} (Score: {profile.security_score}/100)
</option>
))}
</optgroup>
{/* ... more profiles ... */}
</select>
);
}
Event Handler Analysis:
- ✅
onChangehandler is correctly defined - ✅ Handler correctly parses "0" as null (unselect)
- ✅ Handler correctly parses numeric values
- ✅ Handler updates
formDatastate correctly - ❌ THE PROBLEM: Parent modal overlay prevents click event from reaching the
<select>element
State Management Flow:
ProxyHostForm
↓ formData.security_header_profile_id (state)
↓ onChange: setFormData({...formData, security_header_profile_id: value})
↓
ProxyHosts page (parent)
↓ handleSubmit() → updateHost()/createHost()
State Flow Status: ✅ Correct - No issues with state management or event handlers
Detailed Component Context
ProxyHostForm Modal Wrapper
File: frontend/src/components/ProxyHostForm.tsx
Current Structure (PROBLEMATIC):
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
{/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PROBLEM: This div covers entire viewport with pointer events at z-50.
It contains the form, which contains the dropdowns.
Native select menus might render below this z-50, making them unclickable.
*/}
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-800">
<h2 className="text-2xl font-bold text-white">
{host ? 'Edit Proxy Host' : 'Add Proxy Host'}
</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* ... form fields including dropdowns ... */}
<AccessListSelector
value={formData.access_list_id || null}
onChange={id => setFormData({ ...formData, access_list_id: id })}
/>
<select>
{/* Security Headers dropdown */}
</select>
</form>
</div>
</div>
)
Z-Index Analysis:
- Outer wrapper:
z-50withfixed inset-0(covers viewport) - Native
<select>dropdown: Rendered at default z-index (likely less than 50) - Result: Dropdown appears behind or is blocked by the overlay
Parent Component: ProxyHosts Page
File: frontend/src/pages/ProxyHosts.tsx
How It Renders the Form:
export default function ProxyHosts() {
const [showForm, setShowForm] = useState(false)
const [editingHost, setEditingHost] = useState<ProxyHost | undefined>()
const handleAdd = () => {
setEditingHost(undefined)
setShowForm(true)
}
const handleEdit = (host: ProxyHost) => {
setEditingHost(host)
setShowForm(true)
}
const handleSubmit = async (data: Partial<ProxyHost>) => {
if (editingHost) {
await updateHost(editingHost.uuid, data)
} else {
await createHost(data)
}
setShowForm(false)
setEditingHost(undefined)
}
// In render:
return (
<>
{showForm && (
<ProxyHostForm
host={editingHost}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingHost(undefined)
}}
/>
)}
</>
)
}
Parent's Role: ✅ Correct - parent just manages state and renders form conditionally
CSS and Styling Analysis
Modal Overlay Classes Breakdown
Problematic CSS Classes in ProxyHostForm:
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
^^^^^^^ ^^^^
fixed positioning with full viewport coverage Z-index layer
Class Meanings:
fixed: Position relative to viewportinset-0: Applies top, right, bottom, left = 0 (covers entire screen)bg-black/50: Semi-transparent black background (50% opacity)flex items-center justify-center: Centers child contentp-4: Paddingz-50: Tailwind z-index value (z-index: 50 in actual CSS)
CSS Cascade Issue:
/* Outer wrapper (the problem) */
.fixed.inset-0.z-50 {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 50;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
/* Native select inside - inherits parent's stacking context */
select {
/* No explicit z-index */
/* Dropdown menu spawned at default z-index, likely below parent's z-50 */
}
Why Native Selects Fail:
- Browser spawns
<select>dropdown menu in a popup layer - This popup layer has default z-index (often 0 or auto)
- Parent's z-index: 50 creates a new stacking context
- Dropdown menu tries to render in this context
- If dropdown z-index < parent z-index, it's hidden behind the overlay
Test Evidence & References
E2E Test File Evidence
File: tests/integration/proxy-acl-integration.spec.ts
Test Attempting to Select ACL (currently failing):
await test.step('Assign IP-based whitelist ACL to proxy host', async () => {
// Find the proxy host row and click edit
const proxyRow = page.locator(SELECTORS.proxyRow).filter({
hasText: createdDomain,
});
await expect(proxyRow).toBeVisible();
const editButton = proxyRow.locator(SELECTORS.proxyEditBtn).first();
await editButton.click();
await waitForModal(page, /edit|proxy/i);
// Try to select ACL from dropdown
const aclDropdown = page.locator(SELECTORS.aclSelectDropdown);
const aclCombobox = page.getByRole('combobox')
.filter({ hasText: /No Access Control|whitelist/i });
// Build the pattern to match the ACL name
const aclNamePattern = aclConfig.name;
if (await aclDropdown.isVisible()) {
const option = aclDropdown.locator('option').filter({ hasText: aclNamePattern });
const optionValue = await option.getAttribute('value');
if (optionValue) {
await aclDropdown.selectOption({ value: optionValue });
// ❌ THIS FAILS: selectOption doesn't work because overlay blocks interaction
}
}
});
Test Selector References (tests/integration/proxy-acl-integration.spec.ts):
const SELECTORS = {
aclSelectDropdown: '[data-testid="acl-select"], select[name="access_list_id"]',
// ...
}
Known Pattern Reference: ConfigReloadOverlay
Similar problematic pattern in frontend/src/components/LoadingStates.tsx:
export function ConfigReloadOverlay({
message = 'Ferrying configuration...',
submessage = 'Charon is crossing the Styx',
type = 'charon',
}: {
message?: string
submessage?: string
type?: 'charon' | 'coin' | 'cerberus'
}) {
return (
<div className="fixed inset-0 bg-slate-900/70 backdrop-blur-sm flex items-center justify-center z-50"
data-testid="config-reload-overlay">
{/* Content */}
</div>
)
}
Test Helper Workaround (tests/utils/ui-helpers.ts):
/**
* ✅ FIX P0: Wait for ConfigReloadOverlay to disappear before clicking
* The ConfigReloadOverlay component (z-50) intercepts pointer events
* during Caddy config reloads, blocking all interactions.
*/
export async function clickSwitch(
locator: Locator,
options: SwitchOptions = {}
): Promise<void> {
// ...
const page = locator.page();
const overlay = page.locator('[data-testid="config-reload-overlay"]');
// Wait for overlay to disappear before clicking
await overlay.waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {
// Overlay not present - continue
});
// ... click the element ...
}
This confirms the pattern is known to cause problems and requires workarounds.
Detailed Fix Plan
Solution Strategy
Overall Approach: Restructure the modal DOM to separate the overlay from the form content, using proper z-index layering and pointer-events management.
Fix 1: Restructure Modal Z-Index Hierarchy (CRITICAL)
File to Modify: frontend/src/components/ProxyHostForm.tsx
Current Code (Lines 514-525):
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-800">
<h2 className="text-2xl font-bold text-white">
{host ? 'Edit Proxy Host' : 'Add Proxy Host'}
</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
Proposed Fix:
return (
<>
{/* Layer 1: Overlay (z-40) - separate from form to allow form to float above */}
<div className="fixed inset-0 bg-black/50 z-40" onClick={onCancel} />
{/* Layer 2: Form container (z-50) with pointer-events-none on wrapper */}
<div className="fixed inset-0 flex items-center justify-center p-4 pointer-events-none z-50">
{/* Layer 3: Form content with pointer-events-auto to re-enable interactions */}
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-y-auto pointer-events-auto">
<div className="p-6 border-b border-gray-800">
<h2 className="text-2xl font-bold text-white">
{host ? 'Edit Proxy Host' : 'Add Proxy Host'}
</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 pointer-events-auto">
Why This Works:
-
Separate Overlay Layer (
z-40):- Handles backdrop click-to-close
- Doesn't interfere with form's z-index stacking
- Can receive click events without blocking
-
Form Container Layer (
z-50withpointer-events-none):- Higher z-index than overlay ensures form is on top visually
pointer-events-nonemeans this div doesn't capture clicks- Clicks pass through to children
-
Form Content (
pointer-events-auto):- Re-enables pointer events on actual form elements
- Allows native
<select>dropdowns to work properly - Native dropdown menus now render above both overlay and form wrapper
Z-Index Diagram:
z-50: Form Container (pointer-events-none)
├─ Form Content (pointer-events-auto)
│ ├─ Input fields
│ └─ Select dropdowns ← DROPDOWN MENUS RENDER HERE
z-40: Overlay (pointer-events-auto)
Fix 2: Ensure Form Element Pointer-Events
File to Modify: frontend/src/components/ProxyHostForm.tsx
Current Code:
<form onSubmit={handleSubmit} className="p-6 space-y-6">
Updated Code:
<form onSubmit={handleSubmit} className="p-6 space-y-6 pointer-events-auto">
Reasoning:
- Explicitly declares that this form element and its children should receive pointer events
- Provides clear documentation of intent in the code
- Ensures no CSS inheritance quirks prevent dropdown interaction
Fix 3: Add CSS Utilities for Future Select Elements (OPTIONAL ENHANCEMENT)
File to Modify: frontend/src/index.css
Add CSS Rule (at appropriate location in utilities section):
/* Ensure native select dropdowns render above overlays */
@layer components {
/* Native select dropdown positioning */
select {
position: relative;
/* Allow natural z-index stacking - browser will handle dropdown placement */
}
}
Note: This is optional but provides defensive CSS for future modals using the same pattern.
Implementation Checklist
Pre-Implementation
- Read the entire fix plan
- Understand the z-index layering issue
- Understand pointer-events CSS property
- Have access to frontend/src/components/ProxyHostForm.tsx
Implementation Steps
Step 1: Restructure Modal HTML (5-10 minutes)
- Open frontend/src/components/ProxyHostForm.tsx
- Find the return statement (around line 514)
- Split the single overlay-form div into three layers as specified above
- Change outer div:
fixed inset-0 bg-black/50 flex ... z-50→fixed inset-0 bg-black/50 z-40 - Add new container:
fixed inset-0 flex ... z-50 pointer-events-none - Add form wrapper:
pointer-events-autoto content div
Step 2: Update Form Element (1-2 minutes)
- Add
pointer-events-autoto<form>element className - Verify className is properly formatted:
"p-6 space-y-6 pointer-events-auto"
Step 3: Optional - Add CSS Utilities (2-3 minutes)
- Open frontend/src/index.css
- Add defensive CSS for select elements (optional)
Testing (30-60 minutes)
Manual Testing
- Start the dev server:
npm run dev - Navigate to
/proxy-hosts - Click "Add Proxy Host" button
- Verify ACL dropdown opens and responds to clicks
- Select an ACL option
- Verify selection appears in dropdown
- Click on Security Headers dropdown
- Verify dropdown opens and responds to clicks
- Select a security profile
- Verify selection appears in dropdown
- Fill in remaining form fields
- Click Save
- Verify form submits successfully
- Repeat for "Edit Proxy Host" page
Browser Testing
- Test on Chrome/Chromium
- Test on Firefox
- Test on Safari (if available)
- Test on Edge (if available)
E2E Test Execution
- Run:
npm run test:e2e - Verify all tests pass
- Specifically verify proxy-acl-integration tests pass
Unit Test Execution
- Run:
npm run test - Specifically run ProxyHostForm tests:
frontend/src/components/__tests__/ProxyHostForm.test.tsx
- Verify all tests pass
Accessibility Verification
- Test keyboard navigation through form (Tab key)
- Verify you can navigate to dropdowns with Tab
- Verify you can open dropdowns with Enter/Space
- Verify you can navigate options with arrow keys
- Verify you can select with Enter
Code Review Checklist
- Changes are minimal and focused
- No unintended side effects
- All dropdowns tested
- Modal close behavior still works
- No console errors
- No console warnings
- Consistent with existing code style
Merge Checklist
- All tests passing
- All manual testing complete
- Code review approved
- Commit message references this bug report
- No merge conflicts
Testing Strategy
Unit Tests (Frontend)
Test File: frontend/src/components/tests/ProxyHostForm.test.tsx
Commands:
# Run all ProxyHostForm tests
npm run test -- ProxyHostForm.test.tsx
# Run specific test
npm run test -- ProxyHostForm.test.tsx -t "ACL dropdown"
Expected Behavior: All existing tests should pass without modification
E2E Tests (Playwright)
Test File: tests/integration/proxy-acl-integration.spec.ts
Command:
# Run ACL integration tests
npx playwright test proxy-acl-integration.spec.ts
# Run specific test
npx playwright test proxy-acl-integration.spec.ts -g "Assign IP-based"
Expected Results:
- ✅
selectOption()method works on ACL dropdown - ✅ Selection persists in form state
- ✅ Form submits with selected ACL
Manual Verification Checklist
Location: ProxyHosts page (/proxy-hosts)
Scenario 1: Add Proxy Host with ACL
- Click "Add Proxy Host"
- Modal opens
- Fill in required fields (name, domain, forward_host, etc.)
- Click ACL dropdown
- Dropdown opens and is visible above modal
- Select an ACL from the list
- Selection appears in dropdown
- Click Save
- Form submits successfully
- New host appears in table with ACL badge
Scenario 2: Add Proxy Host with Security Headers
- Click "Add Proxy Host"
- Modal opens
- Fill in required fields
- Click Security Headers dropdown
- Dropdown opens and is visible above modal
- Select a security profile
- Selection appears in dropdown
- Click Save
- Form submits successfully
- Verify security headers applied (check browser DevTools)
Scenario 3: Edit Proxy Host - Change ACL
- Click Edit button on existing proxy host
- Modal opens with current values
- Click ACL dropdown
- Dropdown opens and shows current selection
- Select different ACL
- Selection updates in dropdown
- Click Save
- Verify API request includes new ACL
- Verify ACL badge updates in table
Scenario 4: Modal Close Behavior
- Open Add/Edit form
- Click overlay (dark area outside form)
- Modal closes without saving
- Click Edit again
- Open form
- Click X button in top right
- Modal closes without saving
Risk Assessment
Low-Risk Changes ✅
- ✅ Adding
pointer-events-autoclasses (non-breaking CSS) - ✅ Restructuring div hierarchy (same functionality, different structure)
- ✅ Adding
onClick={onCancel}to overlay (alternative way to close, same result)
Medium-Risk Areas ⚠️
- ⚠️ Changing z-index values (could affect other components using same classes)
- ⚠️ Splitting overlay and form (must maintain visual appearance)
- ⚠️ Using Fragment (
<>) instead of wrapper div (may affect animation/transition)
Risk Mitigation
- Test on all browsers
- Run full E2E test suite
- Manual testing on add and edit pages
- Visual regression testing (compare before/after)
- Check for similar patterns in other components
Success Criteria
Functional Requirements ✅
ACL Dropdown:
- ✅ Dropdown opens on click
- ✅ Options are visible and selectable
- ✅ Selection updates form state correctly
- ✅ Selected value persists when saving proxy host
- ✅ ACL is applied to proxy host in backend
Security Headers Dropdown:
- ✅ Dropdown opens on click
- ✅ Options are visible and selectable
- ✅ Selection updates form state correctly
- ✅ Selected value persists when saving proxy host
- ✅ Security headers are applied to proxy host in backend
Modal Behavior:
- ✅ Form submits successfully with selected dropdowns
- ✅ Modal closes on successful save
- ✅ Modal can be closed by clicking overlay
- ✅ Modal can be closed by clicking Cancel button
- ✅ Modal can be closed by clicking X button
Non-Functional Requirements ✅
- ✅ No performance regression
- ✅ No accessibility regression
- ✅ Keyboard navigation works (Tab, Arrow, Enter)
- ✅ No console errors or warnings
- ✅ Consistent with existing UI patterns
Testing Requirements ✅
- ✅ All existing unit tests pass without modification
- ✅ All E2E tests pass (particularly proxy-acl-integration)
- ✅ Manual testing on all supported browsers
- ✅ No JavaScript errors in console
File Summary Table
| File | Action | Complexity | Impact |
|---|---|---|---|
| frontend/src/components/ProxyHostForm.tsx | MUST MODIFY - Restructure modal z-index | Medium | Fixes dropdown interaction |
| frontend/src/components/ProxyHostForm.tsx | MUST MODIFY - Add pointer-events-auto to form | Low | Ensures event propagation |
| frontend/src/components/AccessListSelector.tsx | REVIEW ONLY - No changes needed | N/A | Verify handler is correct |
| frontend/src/pages/ProxyHosts.tsx | REVIEW ONLY - No changes needed | N/A | Verify parent rendering is correct |
| frontend/src/index.css | OPTIONAL - Add select CSS utilities | Low | Future-proofs similar issues |
| tests/integration/proxy-acl-integration.spec.ts | VERIFY - Run tests after fix | N/A | Confirm fix resolves blocking |
Related Patterns to Monitor
ConfigReloadOverlay Pattern
The ConfigReloadOverlay component (frontend/src/components/LoadingStates.tsx) has the same problematic z-index pattern. After fixing ProxyHostForm, consider applying the same solution to ConfigReloadOverlay for consistency.
Other Modal Forms
Scan the codebase for other modal-based forms that might have the same issue:
- Create/Edit dialogs
- Settings forms
- Configuration modals
Check if they use the same fixed inset-0 z-50 pattern and apply fixes as needed.
Timeline Estimate
| Task | Time | Status |
|---|---|---|
| Understand problem | 30 min | ✅ Done |
| Implement fixes | 15-20 min | → Next |
| Manual testing | 45 min | → Next |
| E2E test execution | 10 min | → Next |
| Code review | 15 min | → Next |
| Total | ~2 hours | → Estimated |
Commit Message Template
fix: allow ACL and Security Headers dropdown selection in proxy host form
The ProxyHostForm modal used a full-screen overlay with z-index: 50 that
blocked pointer events to child elements. Native <select> dropdowns
couldn't open or receive clicks because the overlay intercepted events.
Changes:
- Restructured modal DOM to separate overlay (z-40) from form (z-50)
- Added pointer-events-none to form container, pointer-events-auto to
form content to re-enable child interactions
- This allows native <select> dropdowns to render and respond to clicks
Fixes dropdown interaction on both Add and Edit Proxy Host pages.
Related: proxy-acl-integration.spec.ts tests now pass
Appendix: Z-Index Reference
Tailwind Z-Index Scale:
z-0: 0
z-10: 10
z-20: 20
z-30: 30
z-40: 40 ← Overlay (backdrop)
z-50: 50 ← Form (content)
z-auto: auto
Browser Native Elements:
- Select dropdown menu: auto (default z-index)
- Tooltip: varies by browser
- Popup menu: varies by browser
Plan Status: ✅ Complete and Ready for Implementation
Estimated Time to Fix: 2 hours (including testing)
Priority: CRITICAL (blocks ACL and Security Headers assignment)
Severity: HIGH (UI feature completely non-functional)