Merge remote-tracking branch 'origin/pr/561' into codex/pr-561-562-559

This commit is contained in:
Codex Integration
2026-09-12 15:53:44 -06:00
2 changed files with 77 additions and 4 deletions
@@ -0,0 +1,56 @@
import React from 'react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import AdvancedFilterModal, { MAX_RENDERED_OPTIONS } from '@/components/AdvancedFilterModal';
vi.mock('@/lib/motion', () => ({
motion: {
div: ({ children, ...props }: React.ComponentProps<'div'> & Record<string, unknown>) => {
const rest = { ...props };
for (const key of ['initial', 'animate', 'exit', 'transition']) delete rest[key];
return <div {...(rest as React.ComponentProps<'div'>)}>{children}</div>;
},
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
const names = Array.from({ length: 1000 }, (_, i) => `VESSEL ${String(i).padStart(4, '0')}`);
function renderModal() {
return render(
<AdvancedFilterModal
title="MARITIME VESSELS"
icon={null}
accentColor="#3B82F6"
accentColorName="blue"
fields={[{ key: 'ship_name', label: 'VESSEL NAME', options: names }]}
activeFilters={{}}
onApply={vi.fn()}
onClose={vi.fn()}
/>,
);
}
describe('AdvancedFilterModal large option lists', () => {
afterEach(() => cleanup());
it('mounts only the head of a long list and says how much is hidden', () => {
renderModal();
const rows = screen.getAllByRole('button').filter((b) => b.textContent?.startsWith('VESSEL '));
expect(rows).toHaveLength(MAX_RENDERED_OPTIONS);
expect(screen.getByText('1000 AVAILABLE')).toBeTruthy();
expect(screen.getByText(/SHOWING 300 OF 1,000/)).toBeTruthy();
});
it('renders the whole list once the search narrows it', () => {
renderModal();
fireEvent.change(screen.getByPlaceholderText(/Search vessel name/i), {
target: { value: 'VESSEL 09' },
});
const rows = screen.getAllByRole('button').filter((b) => b.textContent?.startsWith('VESSEL '));
expect(rows).toHaveLength(100);
expect(screen.queryByText(/SHOWING/)).toBeNull();
expect(screen.getByText('100 AVAILABLE')).toBeTruthy();
});
});
@@ -1,6 +1,6 @@
'use client';
import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
import { useState, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
import { motion } from '@/lib/motion';
import { Search, X, Check, GripHorizontal } from 'lucide-react';
@@ -11,6 +11,11 @@ interface FilterField {
optionLabels?: Record<string, string>;
}
// Option lists come straight from live data (vessel names run to tens of
// thousands); rendering every row as a button makes the dialog take seconds
// to open, so only the head of the list is mounted until the search narrows it.
export const MAX_RENDERED_OPTIONS = 300;
interface AdvancedFilterModalProps {
title: string;
icon: React.ReactNode;
@@ -55,8 +60,8 @@ export default function AdvancedFilterModal({
const dragStartRef = useRef({ x: 0, y: 0, posX: 0, posY: 0 });
const modalRef = useRef<HTMLDivElement>(null);
// Center on mount, clamped so it doesn't overlap the bottom status bar (~48px)
useEffect(() => {
// Center before first paint, clamped so it doesn't overlap the bottom status bar (~48px)
useLayoutEffect(() => {
if (modalRef.current) {
const rect = modalRef.current.getBoundingClientRect();
const pad = 52; // status bar + small gap
@@ -154,6 +159,12 @@ export default function AdvancedFilterModal({
});
}, [activeField, activeTab, searchTerms]);
const visibleOptions = useMemo(
() => filteredOptions.slice(0, MAX_RENDERED_OPTIONS),
[filteredOptions],
);
const hiddenCount = filteredOptions.length - visibleOptions.length;
// Tailwind color map for dynamic classes
const colorMap: Record<
string,
@@ -355,7 +366,7 @@ export default function AdvancedFilterModal({
</div>
) : (
<div className="flex flex-col gap-px">
{filteredOptions.map((option) => {
{visibleOptions.map((option) => {
const isChecked = draft[activeTab]?.has(option);
return (
<button
@@ -383,6 +394,12 @@ export default function AdvancedFilterModal({
</button>
);
})}
{hiddenCount > 0 && (
<div className="text-center py-3 text-[var(--text-muted)] text-[10px] tracking-widest">
SHOWING {visibleOptions.length.toLocaleString()} OF{' '}
{filteredOptions.length.toLocaleString()} SEARCH TO NARROW
</div>
)}
</div>
)}
</div>