bug-fix/filter dropdowns in dark mode
This commit is contained in:
@@ -60,6 +60,81 @@ function getProjectFinancials(project) {
|
|||||||
const fmt = (amt) =>
|
const fmt = (amt) =>
|
||||||
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0);
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0);
|
||||||
|
|
||||||
|
// Dark-mode-aware dropdown — replaces native <select> so the option list
|
||||||
|
// doesn't render with the OS default white background in dark mode.
|
||||||
|
function FilterPopover({ id, value, onChange, options, allLabel, ariaLabel }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const boxRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onClick = (e) => {
|
||||||
|
if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false);
|
||||||
|
};
|
||||||
|
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||||
|
document.addEventListener('mousedown', onClick);
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', onClick);
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const display = value === 'all' ? allLabel : value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" ref={boxRef}>
|
||||||
|
<button
|
||||||
|
id={id}
|
||||||
|
type="button"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={() => setOpen(o => !o)}
|
||||||
|
className="w-full flex items-center justify-between gap-2 pl-3 pr-2 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="truncate text-left">{display}</span>
|
||||||
|
<ChevronDown size={14} className={`shrink-0 text-zinc-400 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
role="listbox"
|
||||||
|
className="absolute z-30 left-0 right-0 mt-1 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 shadow-xl shadow-black/10 dark:shadow-black/50 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="max-h-56 overflow-y-auto custom-scrollbar py-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={value === 'all'}
|
||||||
|
onClick={() => { onChange('all'); setOpen(false); }}
|
||||||
|
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${value === 'all' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
|
||||||
|
>
|
||||||
|
<span>{allLabel}</span>
|
||||||
|
{value === 'all' && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
|
||||||
|
</button>
|
||||||
|
{options.map(opt => {
|
||||||
|
const isSelected = opt === value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
onClick={() => { onChange(opt); setOpen(false); }}
|
||||||
|
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${isSelected ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{opt}</span>
|
||||||
|
{isSelected && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CommissionDistributionPanel = ({
|
const CommissionDistributionPanel = ({
|
||||||
ownerProjects,
|
ownerProjects,
|
||||||
orgCommissionDefaults,
|
orgCommissionDefaults,
|
||||||
@@ -173,9 +248,6 @@ const CommissionDistributionPanel = ({
|
|||||||
setProjectQuery('');
|
setProjectQuery('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectClass =
|
|
||||||
"appearance-none w-full pl-3 pr-8 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-colors cursor-pointer";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -220,21 +292,14 @@ const CommissionDistributionPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Role filter */}
|
{/* Role filter */}
|
||||||
<div className="relative">
|
<FilterPopover
|
||||||
<label htmlFor="commission-role-filter" className="sr-only">Filter by role</label>
|
|
||||||
<select
|
|
||||||
id="commission-role-filter"
|
id="commission-role-filter"
|
||||||
|
ariaLabel="Filter by role"
|
||||||
value={roleFilter}
|
value={roleFilter}
|
||||||
onChange={(e) => setRoleFilter(e.target.value)}
|
onChange={setRoleFilter}
|
||||||
className={selectClass}
|
options={roleOptions}
|
||||||
>
|
allLabel="All Roles"
|
||||||
<option value="all">All Roles</option>
|
/>
|
||||||
{roleOptions.map(r => (
|
|
||||||
<option key={r} value={r}>{r}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Project searchable dropdown */}
|
{/* Project searchable dropdown */}
|
||||||
<div className="relative" ref={projectBoxRef}>
|
<div className="relative" ref={projectBoxRef}>
|
||||||
@@ -295,21 +360,14 @@ const CommissionDistributionPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Commission Type filter */}
|
{/* Commission Type filter */}
|
||||||
<div className="relative">
|
<FilterPopover
|
||||||
<label htmlFor="commission-type-filter" className="sr-only">Filter by commission type</label>
|
|
||||||
<select
|
|
||||||
id="commission-type-filter"
|
id="commission-type-filter"
|
||||||
|
ariaLabel="Filter by commission type"
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(e) => setTypeFilter(e.target.value)}
|
onChange={setTypeFilter}
|
||||||
className={selectClass}
|
options={typeOptions}
|
||||||
>
|
allLabel="All Commission Types"
|
||||||
<option value="all">All Commission Types</option>
|
/>
|
||||||
{typeOptions.map(t => (
|
|
||||||
<option key={t} value={t}>{t}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<div className="sm:col-span-2 lg:col-span-4 flex justify-end">
|
<div className="sm:col-span-2 lg:col-span-4 flex justify-end">
|
||||||
|
|||||||
Reference in New Issue
Block a user