| 1 | import { useRef, useState } from "react"; |
| 2 | import { Check, ChevronDown, Play } from "lucide-react"; |
| 3 | import { AnchoredPopover } from "./AnchoredPopover"; |
| 4 | import { useT } from "../lib/i18n"; |
| 5 | import type { DictKey } from "../lib/i18n"; |
| 6 | import type { SoundWavPref } from "../lib/sound"; |
| 7 | |
| 8 | type SoundOption = { |
| 9 | value: SoundWavPref; |
| 10 | labelKey: DictKey; |
| 11 | }; |
| 12 | |
| 13 | const OPTIONS: SoundOption[] = [ |
| 14 | { value: "off", labelKey: "settings.notificationSound.off" }, |
| 15 | { value: "synth", labelKey: "settings.notificationSound.synth" }, |
| 16 | { value: "positive", labelKey: "settings.notificationSound.positive" }, |
| 17 | { value: "correct", labelKey: "settings.notificationSound.correct" }, |
| 18 | { value: "start", labelKey: "settings.notificationSound.start" }, |
| 19 | { value: "back", labelKey: "settings.notificationSound.back" }, |
| 20 | ]; |
| 21 | |
| 22 | export function SoundSelect({ |
| 23 | value, |
| 24 | onChange, |
| 25 | onPreview, |
| 26 | previewDisabled, |
| 27 | }: { |
| 28 | value: SoundWavPref; |
| 29 | onChange: (v: SoundWavPref) => void; |
| 30 | onPreview: () => void; |
| 31 | previewDisabled?: boolean; |
| 32 | }) { |
| 33 | const t = useT(); |
| 34 | const [open, setOpen] = useState(false); |
| 35 | const triggerRef = useRef<HTMLButtonElement>(null); |
| 36 | const selected = OPTIONS.find((o) => o.value === value) ?? OPTIONS[0]; |
| 37 | |
| 38 | return ( |
| 39 | <div className="sound-select"> |
| 40 | <button |
| 41 | ref={triggerRef} |
| 42 | className="sound-select__trigger" |
| 43 | type="button" |
| 44 | onClick={() => setOpen((v) => !v)} |
| 45 | > |
| 46 | <span className="sound-select__label">{t(selected.labelKey)}</span> |
| 47 | <ChevronDown |
| 48 | size={16} |
| 49 | className={`sound-select__chev${open ? " sound-select__chev--open" : ""}`} |
| 50 | /> |
| 51 | </button> |
| 52 | {!previewDisabled && ( |
| 53 | <button className="chip chip--icon" type="button" title={t("settings.notificationSoundPreview")} aria-label={t("settings.notificationSoundPreview")} onClick={onPreview}> |
| 54 | <Play size={13} aria-hidden="true" /> |
| 55 | </button> |
| 56 | )} |
| 57 | <AnchoredPopover |
| 58 | open={open} |
| 59 | anchorRef={triggerRef} |
| 60 | onClose={() => setOpen(false)} |
| 61 | className="sound-select__menu" |
| 62 | placement="bottom" |
| 63 | > |
| 64 | <div className="sound-select__list" role="listbox"> |
| 65 | {OPTIONS.map((opt) => ( |
| 66 | <button |
| 67 | key={opt.value} |
| 68 | className={`sound-select__option${opt.value === value ? " sound-select__option--selected" : ""}`} |
| 69 | role="option" |
| 70 | aria-selected={opt.value === value} |
| 71 | type="button" |
| 72 | onClick={() => { |
| 73 | onChange(opt.value); |
| 74 | setOpen(false); |
| 75 | }} |
| 76 | > |
| 77 | <span>{t(opt.labelKey)}</span> |
| 78 | {opt.value === value && <Check size={14} className="sound-select__check" />} |
| 79 | </button> |
| 80 | ))} |
| 81 | </div> |
| 82 | </AnchoredPopover> |
| 83 | </div> |
| 84 | ); |
| 85 | } |
| 86 |