From d3a783f77fb9f38d5f0b768fd6d73bb1a89dbf9a Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 21 Sep 2026 15:25:21 +0200 Subject: [PATCH] feat: custom item list in selector --- README.md | 1 + src/components/ChoiceSelect/ChoiceSelect.css | 105 ++++++++--- src/components/ChoiceSelect/ChoiceSelect.tsx | 163 ++++++++++++++---- .../MiscellaneousSection.tsx | 2 + src/pages/AddDspPage/AddDspPage.css | 5 + src/pages/AddDspPage/AddDspPage.tsx | 2 +- 6 files changed, 223 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 28fbd0a..c183784 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,6 @@ This template should help get you started developing with Tauri, React and Types - Dont draw geq/peq on gain if bypass - Diplay windows on reset geq, peq - Preset change dont reset +- Disbale slider on peq when filter dont use it - Check end to end behavioour \ No newline at end of file diff --git a/src/components/ChoiceSelect/ChoiceSelect.css b/src/components/ChoiceSelect/ChoiceSelect.css index 97d5524..33f38a7 100644 --- a/src/components/ChoiceSelect/ChoiceSelect.css +++ b/src/components/ChoiceSelect/ChoiceSelect.css @@ -1,65 +1,120 @@ +/* ChoiceSelect.css */ .choice-select { position: relative; display: flex; - align-items: stretch; + + color: var(--accent-brand); } .choice-select[data-disabled] { opacity: 0.5; } -.choice-select__input { +.choice-select__trigger { flex: 1 1 auto; + width: 100%; + height: 100%; - height: var(--card-control-height); - padding: 0 var(--space-6) 0 var(--space-3); + display: flex; + align-items: center; + justify-content: space-between; + + padding: 0; + padding-inline: 5%; background: transparent; - color: var(--text-primary); border: var(--border-width) solid var(--border-hairline); border-radius: var(--radius-sm); font-family: var(--font-ui); - font-size: var(--font-sm); + font-size: 90%; + + color: inherit; - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - - outline: none; cursor: pointer; - transition: border-color var(--dur-base) var(--ease-standard); } -.choice-select__input:focus { +.choice-select__trigger:hover, +.choice-select__trigger--open { border-color: var(--accent-brand); } -.choice-select__input:disabled { +.choice-select__trigger:disabled { cursor: not-allowed; } +.choice-select__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .choice-select__chevron { - position: absolute; - top: 0; - right: 2%; - bottom: 0; - - width: 4cqh; - padding-top: 3%; - padding-bottom: 3%; - + flex-shrink: 0; display: flex; align-items: center; justify-content: center; - pointer-events: none; + width: 4cqh; + aspect-ratio: 1; + color: var(--accent-brand); + transition: transform var(--dur-base) var(--ease-standard); +} + +.choice-select__trigger--open .choice-select__chevron { + transform: rotate(180deg); } .choice-select__chevron svg { - width: 75%; + width: 50%; height: auto; display: block; } + +.choice-select__option { + padding: 1%; + border-radius: var(--radius-sm); + font-size: 100%; + color: var(--text-primary); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.choice-select__option--highlight { + background: var(--bg-raised); +} + +.choice-select__option--selected { + color: var(--accent-brand); + font-weight: var(--font-weight-semibold); +} + +.choice-select__listbox { + position: absolute; + left: 0; + right: 0; + z-index: 40; + + top: calc(100% + var(--space-1)); + + margin: 0; + padding: var(--space-1); + list-style: none; + + background: var(--bg-panel); + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-sm); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + + max-height: 240px; + overflow-y: auto; +} + +.choice-select__listbox--up { + top: auto; + bottom: calc(100% + var(--space-1)); +} diff --git a/src/components/ChoiceSelect/ChoiceSelect.tsx b/src/components/ChoiceSelect/ChoiceSelect.tsx index 4c2ca43..6546a09 100644 --- a/src/components/ChoiceSelect/ChoiceSelect.tsx +++ b/src/components/ChoiceSelect/ChoiceSelect.tsx @@ -1,4 +1,5 @@ // ChoiceSelect.tsx +import { useEffect, useRef, useState } from 'react'; import { TriangleDownIcon } from '../../assets/icons/Triangle'; import './ChoiceSelect.css'; @@ -13,6 +14,8 @@ type ChoiceSelectProps = { style?: React.CSSProperties; }; +const LISTBOX_MAX_HEIGHT = 240; // keep in sync with .choice-select__listbox max-height + function ChoiceSelect({ value, options, @@ -23,50 +26,152 @@ function ChoiceSelect({ className = '', style, }: ChoiceSelectProps) { + const [open, setOpen] = useState(false); + const [highlight, setHighlight] = useState(0); + const [dropUp, setDropUp] = useState(false); + + const rootRef = useRef(null); + const triggerRef = useRef(null); + + useEffect(() => { + function handleClick(e: MouseEvent) { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, []); + + // Re-check placement on scroll/resize while open, not just at open-time — + // the trigger's position can change (e.g. inside a scrollable panel). + useEffect(() => { + if (!open) return; + const recalc = () => computeDropDirection(); + window.addEventListener('scroll', recalc, true); + window.addEventListener('resize', recalc); + return () => { + window.removeEventListener('scroll', recalc, true); + window.removeEventListener('resize', recalc); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + const step = (direction: 1 | -1) => { const currentIndex = options.indexOf(value); - if (currentIndex === -1 || options.length === 0) return; - const nextIndex = (currentIndex + direction + options.length) % options.length; - onChange(options[nextIndex]); }; + function computeDropDirection() { + const el = triggerRef.current; + if (!el) return; + + const rect = el.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + + // Prefer opening down. Only flip up if there's not enough room below + // AND there's more room above — otherwise stay down and let it scroll/clip. + const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow; + setDropUp(needsFlip); + } + + function openList() { + if (disabled) return; + computeDropDirection(); + setOpen(true); + const idx = options.indexOf(value); + setHighlight(idx >= 0 ? idx : 0); + } + + function pick(opt: T) { + if (opt !== value) onChange(opt); + setOpen(false); + triggerRef.current?.focus(); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (disabled) return; + + if (!open) { + if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { + e.preventDefault(); + openList(); + } + return; + } + + if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) { + e.preventDefault(); + setHighlight((h) => Math.min(h + 1, options.length - 1)); + } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) { + e.preventDefault(); + setHighlight((h) => Math.max(h - 1, 0)); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + pick(options[highlight]); + } else if (e.key === 'Escape') { + e.preventDefault(); + setOpen(false); + triggerRef.current?.focus(); + } + } + return ( -
- + {formatLabel(value)} + + - + {open && ( +
    + {options.map((option, i) => ( +
  • setHighlight(i)} + onClick={() => pick(option)} + > + {formatLabel(option)} +
  • + ))} +
+ )}
); } diff --git a/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx b/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx index 64c28e3..b60b42a 100644 --- a/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx +++ b/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx @@ -112,6 +112,7 @@ function MiscellaneousPanel({ value={inputSource.type} options={INPUT_TYPES} onChange={handleInputTypeChange} + style={{color: 'var(--font-primary)'}} /> @@ -131,6 +132,7 @@ function MiscellaneousPanel({ return `${frequency.slice(1)} Hz`; }} onChange={handleFrequencyChange} + style={{color: 'var(--font-primary)'}} /> )} diff --git a/src/pages/AddDspPage/AddDspPage.css b/src/pages/AddDspPage/AddDspPage.css index 0715d0f..929c2a8 100644 --- a/src/pages/AddDspPage/AddDspPage.css +++ b/src/pages/AddDspPage/AddDspPage.css @@ -155,6 +155,11 @@ height: var(--card-control-height); } +.add-dsp-form__selector { + color: var(--text-primary); + height: 100%; +} + .add-dsp-form__field .choice-stepper__input { border: none; background: transparent; diff --git a/src/pages/AddDspPage/AddDspPage.tsx b/src/pages/AddDspPage/AddDspPage.tsx index fc9a943..c715dce 100644 --- a/src/pages/AddDspPage/AddDspPage.tsx +++ b/src/pages/AddDspPage/AddDspPage.tsx @@ -107,7 +107,7 @@ function AddDSPForm({