Amélioration UI
This commit is contained in:
1 parent
f4c590b3a3
commit
f76e7c9932
2 files changed
+142
-10
No files matched your search
@@ -0,0 +1,136 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { fmtEUR } from '../utils/format.js';
|
||||
|
||||
/**
|
||||
* InvestissementSelect — combobox pour le champ "Investissement" de RembFormModal.
|
||||
*
|
||||
* Remplace le <select> natif : le navigateur ne permet pas de limiter la hauteur du popup
|
||||
* d'un <select> natif via CSS, ce qui produisait une liste déroulante démesurément haute
|
||||
* quand beaucoup d'investissements existent.
|
||||
*
|
||||
* Positionnement en `position: fixed` (calculé via getBoundingClientRect, comme InvSelect.jsx) —
|
||||
* pas en `position: absolute` — car ce champ est utilisé dans RembFormModal dont le conteneur
|
||||
* `.card` a `overflow: auto` (voir Modal.jsx) : un dropdown en absolute y serait rogné quand la
|
||||
* fenêtre est basse. Le dropdown bascule aussi au-dessus du champ s'il n'y a pas assez de place
|
||||
* en dessous, et se ferme au scroll/resize (repositionnement non géré en direct, comme InvSelect).
|
||||
*
|
||||
* Props :
|
||||
* value : '' | 'BONUS_PARRAINAGE' | 'BONUS_PLATEFORME' | id (string/number)
|
||||
* onChange : (value) => void
|
||||
* options : investissementOptions — { id, plateforme_nom, nom_projet, montant_investi }[]
|
||||
*/
|
||||
export default function InvestissementSelect({ value, onChange, options = [] }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dropPos, setDropPos] = useState({ left: 0, width: 0, maxHeight: 260, direction: 'down', top: 0, bottom: 0 });
|
||||
|
||||
const wrapRef = useRef(null);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const selectedOption = options.find(i => String(i.id) === String(value));
|
||||
const label = value === 'BONUS_PARRAINAGE' ? '🎁 Bonus Parrainage'
|
||||
: value === 'BONUS_PLATEFORME' ? '🏆 Bonus Plateforme'
|
||||
: selectedOption ? `${selectedOption.plateforme_nom} — ${selectedOption.nom_projet} (${fmtEUR(selectedOption.montant_investi)})`
|
||||
: null;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !triggerRef.current) return;
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const spaceBelow = window.innerHeight - rect.bottom - margin;
|
||||
const spaceAbove = rect.top - margin;
|
||||
const desired = 260; // ~ moitié de la hauteur qu'affichait le <select> natif sans contrainte
|
||||
if (spaceBelow >= 140 || spaceBelow >= spaceAbove) {
|
||||
setDropPos({
|
||||
direction: 'down', left: rect.left, width: rect.width,
|
||||
top: rect.bottom + 4, bottom: 0,
|
||||
maxHeight: Math.max(120, Math.min(desired, spaceBelow)),
|
||||
});
|
||||
} else {
|
||||
setDropPos({
|
||||
direction: 'up', left: rect.left, width: rect.width,
|
||||
top: 0, bottom: window.innerHeight - rect.top + 4,
|
||||
maxHeight: Math.max(120, Math.min(desired, spaceAbove)),
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const close = (e) => {
|
||||
if (wrapRef.current?.contains(e.target)) return;
|
||||
const drop = document.getElementById('investissement-select-dropdown-portal');
|
||||
if (drop?.contains(e.target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
const closeOnScroll = (e) => {
|
||||
const drop = document.getElementById('investissement-select-dropdown-portal');
|
||||
if (drop?.contains(e.target)) return; // scroll dans le dropdown — on garde ouvert
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', close);
|
||||
window.addEventListener('scroll', closeOnScroll, true);
|
||||
window.addEventListener('resize', closeOnScroll);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', close);
|
||||
window.removeEventListener('scroll', closeOnScroll, true);
|
||||
window.removeEventListener('resize', closeOnScroll);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const select = (v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const rowStyle = (active) => ({
|
||||
background: active ? 'var(--surface-2)' : undefined,
|
||||
fontWeight: active ? 600 : 400,
|
||||
color: active ? 'var(--primary)' : undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className="cat-select-wrap">
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className={`cat-select-trigger${open ? ' open' : ''}`}
|
||||
onClick={() => setOpen(o => !o)}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="cat-select-label" style={!label ? { color: 'var(--text-muted)' } : undefined}>
|
||||
{label ?? '—'}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ flexShrink: 0, transition: 'transform .15s', transform: open ? 'rotate(0deg)' : 'rotate(180deg)' }}
|
||||
aria-hidden="true">
|
||||
<path d="M18 15l-6-6-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
id="investissement-select-dropdown-portal"
|
||||
className="cat-select-dropdown"
|
||||
role="listbox"
|
||||
style={{
|
||||
position: 'fixed', left: dropPos.left, width: dropPos.width,
|
||||
maxHeight: dropPos.maxHeight,
|
||||
...(dropPos.direction === 'down' ? { top: dropPos.top } : { bottom: dropPos.bottom }),
|
||||
}}
|
||||
>
|
||||
<button type="button" style={rowStyle(value === '')} onClick={() => select('')}>—</button>
|
||||
<button type="button" style={rowStyle(value === 'BONUS_PARRAINAGE')} onClick={() => select('BONUS_PARRAINAGE')}>🎁 Bonus Parrainage</button>
|
||||
<button type="button" style={rowStyle(value === 'BONUS_PLATEFORME')} onClick={() => select('BONUS_PLATEFORME')}>🏆 Bonus Plateforme</button>
|
||||
<div className="cat-select-sep" />
|
||||
{options.map(i => (
|
||||
<button key={i.id} type="button" style={rowStyle(String(value) === String(i.id))} onClick={() => select(String(i.id))}>
|
||||
{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Modal from './Modal.jsx';
|
||||
import InvestissementSelect from './InvestissementSelect.jsx';
|
||||
import { fmtEUR } from '../utils/format.js';
|
||||
|
||||
/* ── Helpers purs (dupliqués volontairement, pas d'état) ── */
|
||||
@@ -128,16 +129,11 @@ export default function RembFormModal({
|
||||
{!locked && (
|
||||
<div style={{ gridColumn: isBonus ? 'span 1' : 'span 3' }}>
|
||||
<label>Investissement *</label>
|
||||
<select required value={form.investissement_id}
|
||||
onChange={e => onFieldChange('investissement_id', e.target.value)}>
|
||||
<option value="">—</option>
|
||||
<option value="BONUS_PARRAINAGE">🎁 Bonus Parrainage</option>
|
||||
<option value="BONUS_PLATEFORME">🏆 Bonus Plateforme</option>
|
||||
<optgroup label="──────────────────" />
|
||||
{investissementOptions.map(i =>
|
||||
<option key={i.id} value={i.id}>{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})</option>
|
||||
)}
|
||||
</select>
|
||||
<InvestissementSelect
|
||||
value={form.investissement_id}
|
||||
onChange={v => onFieldChange('investissement_id', v)}
|
||||
options={investissementOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!locked && isBonus && (
|
||||
|
||||
Reference in new issue
Block a user